Release v1.7.0: Add repomix-safe-mixer skill

Add new security-focused skill for safely packaging codebases with repomix
by automatically detecting and removing hardcoded credentials.

New skill: repomix-safe-mixer
- Detects 20+ credential patterns (AWS, Supabase, Stripe, OpenAI, etc.)
- Scan → Report → Pack workflow with automatic blocking
- Standalone security scanner for pre-commit hooks
- Environment variable replacement guidance
- JSON output for CI/CD integration

Also updates:
- skill-creator: Simplified path resolution best practices
- marketplace.json: Version 1.7.0, added repomix-safe-mixer plugin
- README.md: Updated to 14 skills, added repomix-safe-mixer documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
daymade
2025-10-26 16:47:35 +08:00
parent de8b803283
commit 8a4c7cfb10
7 changed files with 977 additions and 8 deletions

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""
Safe packaging workflow for repomix.
Scans for secrets, reports findings, and optionally packs after user confirmation.
"""
import os
import sys
import subprocess
import json
from pathlib import Path
def run_secret_scan(directory: Path, exclude_patterns: list = None):
"""Run secret scanner and return findings."""
script_dir = Path(__file__).parent
scan_script = script_dir / 'scan_secrets.py'
cmd = [sys.executable, str(scan_script), str(directory), '--json']
if exclude_patterns:
cmd.extend(['--exclude'] + exclude_patterns)
result = subprocess.run(cmd, capture_output=True, text=True)
try:
findings = json.loads(result.stdout) if result.stdout.strip() else []
except json.JSONDecodeError:
print(f"Error: Could not parse scan results", file=sys.stderr)
print(f"Scanner output: {result.stdout}", file=sys.stderr)
sys.exit(1)
return findings
def print_findings_report(findings: list):
"""Print human-readable findings report."""
if not findings:
print("✅ No secrets detected!\n")
return
print(f"\n⚠️ Security Scan Found {len(findings)} Potential Secrets:\n")
# Group by type
by_type = {}
for finding in findings:
type_name = finding['type']
if type_name not in by_type:
by_type[type_name] = []
by_type[type_name].append(finding)
# Print by type
for secret_type in sorted(by_type.keys()):
count = len(by_type[secret_type])
print(f"🔴 {secret_type}: {count} instance(s)")
for finding in by_type[secret_type][:3]: # Show first 3
print(f" - {finding['file']}:{finding['line']}")
print(f" Match: {finding['match']}")
if len(by_type[secret_type]) > 3:
print(f" ... and {len(by_type[secret_type]) - 3} more\n")
else:
print()
def run_repomix(directory: Path, output_file: Path = None, config_file: Path = None):
"""Run repomix to package the directory."""
cmd = ['repomix']
if config_file and config_file.exists():
cmd.extend(['--config', str(config_file)])
if output_file:
cmd.extend(['--output', str(output_file)])
# Change to directory before running repomix
result = subprocess.run(cmd, cwd=directory, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: repomix failed", file=sys.stderr)
print(result.stderr, file=sys.stderr)
sys.exit(1)
print(result.stdout)
return result
def main():
if len(sys.argv) < 2:
print("Usage: safe_pack.py <directory> [--output file.xml] [--config repomix.config.json] [--force] [--exclude pattern1 pattern2 ...]")
print("\nOptions:")
print(" --output <file> Output file path for repomix")
print(" --config <file> Repomix config file")
print(" --force Skip confirmation, pack anyway (dangerous!)")
print(" --exclude <patterns> Patterns to exclude from secret scanning")
print("\nExamples:")
print(" safe_pack.py ./my-project")
print(" safe_pack.py ./my-project --output package.xml")
print(" safe_pack.py ./my-project --exclude '.*test.*' '.*\.example'")
print(" safe_pack.py ./my-project --force # Dangerous! Skip scan")
sys.exit(1)
directory = Path(sys.argv[1]).resolve()
if not directory.is_dir():
print(f"Error: {directory} is not a directory", file=sys.stderr)
sys.exit(1)
# Parse arguments
output_file = None
config_file = None
force = '--force' in sys.argv
exclude_patterns = []
if '--output' in sys.argv:
output_idx = sys.argv.index('--output')
if output_idx + 1 < len(sys.argv):
output_file = Path(sys.argv[output_idx + 1])
if '--config' in sys.argv:
config_idx = sys.argv.index('--config')
if config_idx + 1 < len(sys.argv):
config_file = Path(sys.argv[config_idx + 1])
if '--exclude' in sys.argv:
exclude_idx = sys.argv.index('--exclude')
exclude_patterns = [
arg for arg in sys.argv[exclude_idx + 1:]
if not arg.startswith('--') and arg != str(directory)
]
print(f"🔍 Scanning {directory} for hardcoded secrets...\n")
# Step 1: Scan for secrets
findings = run_secret_scan(directory, exclude_patterns)
# Step 2: Report findings
print_findings_report(findings)
# Step 3: Decision point
if findings:
if force:
print("⚠️ WARNING: --force flag set, packing anyway despite secrets found!\n")
else:
print("❌ Cannot pack: Secrets detected!")
print("\nRecommended actions:")
print("1. Review the findings above")
print("2. Replace hardcoded credentials with environment variables")
print("3. Run scan_secrets.py to verify cleanup")
print("4. Run this script again")
print("\nOr use --force to pack anyway (NOT RECOMMENDED)")
sys.exit(1)
# Step 4: Pack with repomix
print(f"📦 Packing {directory} with repomix...\n")
run_repomix(directory, output_file, config_file)
print("\n✅ Packaging complete!")
if findings:
print("\n⚠️ WARNING: Package contains secrets (--force was used)")
print(" DO NOT share this package publicly!")
else:
print(" Package is safe to distribute.")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Security scanner for detecting hardcoded credentials in code.
Scans a directory for common credential patterns and reports findings.
"""
import os
import re
import sys
import json
from pathlib import Path
from typing import List, Dict, Tuple
# Common secret patterns (regex)
SECRET_PATTERNS = {
'aws_access_key': r'(?i)AKIA[0-9A-Z]{16}',
'aws_secret_key': r'(?i)(?:aws_secret|aws.{0,20}secret).{0,20}[=:]\s*["\']?([0-9a-zA-Z/+=]{40})["\']?',
'supabase_url': r'https://[a-z]{20}\.supabase\.co',
'supabase_anon_key': r'eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*',
'stripe_key': r'(?:sk|pk)_(live|test)_[0-9a-zA-Z]{24,}',
'cloudflare_api_token': r'(?i)(?:cloudflare|cf).{0,20}(?:token|key).{0,20}[=:]\s*["\']?([a-zA-Z0-9_-]{40,})["\']?',
'turnstile_key': r'0x[0-9A-F]{22}',
'generic_api_key': r'(?i)(?:api[_-]?key|apikey).{0,20}[=:]\s*["\']?([0-9a-zA-Z_\-]{20,})["\']?',
'r2_account_id': r'[0-9a-f]{32}(?=\.r2\.cloudflarestorage\.com)',
'jwt_token': r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}',
'private_key': r'-----BEGIN (?:RSA|EC|OPENSSH|DSA) PRIVATE KEY-----',
'oauth_secret': r'(?i)(?:client_secret|oauth).{0,20}[=:]\s*["\']?([0-9a-zA-Z_\-]{20,})["\']?',
}
# File extensions to scan
SCANNABLE_EXTENSIONS = {
'.ts', '.tsx', '.js', '.jsx', '.py', '.md', '.json', '.yaml', '.yml',
'.env', '.env.example', '.env.local', '.env.production', '.env.development',
'.sh', '.bash', '.zsh', '.sql', '.go', '.java', '.rb', '.php', '.cs'
}
# Directories to skip
SKIP_DIRS = {
'node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build',
'.next', '.nuxt', 'vendor', 'target', 'bin', 'obj', '.terraform'
}
class SecretFinding:
"""Represents a detected secret."""
def __init__(self, file_path: str, line_num: int, pattern_name: str,
matched_text: str, line_content: str):
self.file_path = file_path
self.line_num = line_num
self.pattern_name = pattern_name
self.matched_text = matched_text
self.line_content = line_content.strip()
def to_dict(self) -> Dict:
return {
'file': self.file_path,
'line': self.line_num,
'type': self.pattern_name,
'match': self.matched_text[:50] + '...' if len(self.matched_text) > 50 else self.matched_text,
'context': self.line_content[:100] + '...' if len(self.line_content) > 100 else self.line_content
}
def scan_file(file_path: Path, base_dir: Path) -> List[SecretFinding]:
"""Scan a single file for secrets."""
findings = []
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
for line_num, line in enumerate(f, 1):
for pattern_name, pattern in SECRET_PATTERNS.items():
matches = re.finditer(pattern, line)
for match in matches:
# Skip common false positives
if should_skip_match(line, match.group()):
continue
findings.append(SecretFinding(
file_path=str(file_path.relative_to(base_dir)),
line_num=line_num,
pattern_name=pattern_name,
matched_text=match.group(),
line_content=line
))
except Exception as e:
print(f"Warning: Could not scan {file_path}: {e}", file=sys.stderr)
return findings
def should_skip_match(line: str, match: str) -> bool:
"""Check if a match should be skipped (likely false positive)."""
# Skip example/placeholder values
placeholders = [
'your-', 'example', 'placeholder', 'xxx', 'yyy', 'zzz',
'test-', 'demo-', 'sample-', '<YOUR_', '${', 'TODO'
]
line_lower = line.lower()
match_lower = match.lower()
for placeholder in placeholders:
if placeholder in match_lower or placeholder in line_lower:
return True
# Skip if in a comment
if re.search(r'^\s*(?://|#|/\*|\*)', line):
return True
return False
def scan_directory(directory: Path, exclude_patterns: List[str] = None) -> List[SecretFinding]:
"""Scan a directory recursively for secrets."""
findings = []
exclude_patterns = exclude_patterns or []
for root, dirs, files in os.walk(directory):
# Skip excluded directories
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
root_path = Path(root)
# Skip if matches exclude pattern
if any(re.search(pattern, str(root_path)) for pattern in exclude_patterns):
continue
for file in files:
file_path = root_path / file
# Only scan relevant file types
if file_path.suffix not in SCANNABLE_EXTENSIONS:
continue
# Skip if matches exclude pattern
if any(re.search(pattern, str(file_path)) for pattern in exclude_patterns):
continue
file_findings = scan_file(file_path, directory)
findings.extend(file_findings)
return findings
def print_report(findings: List[SecretFinding], directory: Path):
"""Print a human-readable report."""
if not findings:
print("✅ No secrets detected!")
return
print(f"\n⚠️ Found {len(findings)} potential secrets in {directory}:\n")
# Group by file
by_file = {}
for finding in findings:
if finding.file_path not in by_file:
by_file[finding.file_path] = []
by_file[finding.file_path].append(finding)
# Print grouped
for file_path in sorted(by_file.keys()):
print(f"📄 {file_path}")
for finding in by_file[file_path]:
print(f" Line {finding.line_num}: {finding.pattern_name}")
print(f" Match: {finding.matched_text[:80]}")
print(f" Context: {finding.line_content[:80]}")
print()
def main():
if len(sys.argv) < 2:
print("Usage: scan_secrets.py <directory> [--json] [--exclude pattern1 pattern2 ...]")
print("\nExamples:")
print(" scan_secrets.py ./my-project")
print(" scan_secrets.py ./my-project --json")
print(" scan_secrets.py ./my-project --exclude '.*test.*' '.*example.*'")
sys.exit(1)
directory = Path(sys.argv[1]).resolve()
if not directory.is_dir():
print(f"Error: {directory} is not a directory", file=sys.stderr)
sys.exit(1)
# Parse arguments
json_output = '--json' in sys.argv
exclude_patterns = []
if '--exclude' in sys.argv:
exclude_idx = sys.argv.index('--exclude')
exclude_patterns = [arg for arg in sys.argv[exclude_idx + 1:] if not arg.startswith('--')]
# Scan
findings = scan_directory(directory, exclude_patterns)
# Output
if json_output:
print(json.dumps([f.to_dict() for f in findings], indent=2))
else:
print_report(findings, directory)
# Exit code
sys.exit(1 if findings else 0)
if __name__ == '__main__':
main()