repomix-safe-mixer
Safely package codebases with repomix by automatically detecting and removing hardcoded credentials before packing. Use when packaging code for distribution, creating reference packages, or when the user mentions security concerns about sharing code with repomix.
1实现原理 · 为什么它能做到
编排方式是「子进程 + JSON 契约」:safe_pack.py 不导入扫描器,而是用 subprocess 调同目录的 scan_secrets.py 并加 --json,再解析其 stdout 决定是否打包。
cmd = [sys.executable, str(scan_script), str(directory), '--json']
检测能力来自一张 13 项的正则字典(SECRET_PATTERNS),全部是行内正则匹配,无 AST、无熵值分析、无机器学习。
'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,}',
扫描范围由「扩展名白名单 + .env 文件名特例」双重界定,且代码注释明确解释了为什么必须给 dotenv 开特例(Path('.env').suffix == '')。
# Full filenames that must be scanned regardless of Path.suffix. Dotenv # files break suffix matching: Path('.env').suffix == '' and # Path('.env.local').suffix == '.local', so scanning by extension alone # silently skips the most common places secrets end up. SCANNABLE_FILENAMES = { '.env',
跳过目录是硬编码集合(node_modules/.git/.venv/dist/build 等 15 个),不做 gitignore 解析。
SKIP_DIRS = { 'node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build',
假阳性抑制有两条硬规则:占位符词表命中即跳过,且对整行做 substring 判断(只要该行任意位置出现 'example'/'test-' 等词,本行所有命中都被丢弃)。
placeholders = [ 'your-', 'example', 'placeholder', 'xxx', 'yyy', 'zzz', 'test-', 'demo-', 'sample-', '<YOUR_', '${', 'TODO' ]
注释行一律跳过(行首 // / # / /* / *)。
if re.search(r'^\s*(?://|#|/\*|\*)', line):
默认 fail-closed:命中即拒绝打包并 exit 1,只有显式 --force 才会继续;--force 路径仍打印警告。
print("❌ Cannot pack: Secrets detected!")
打包动作是调用外部 CLI repomix:把目标目录作为 cwd,透传 --config/--output,产物由 repomix 写盘。
result = subprocess.run(cmd, cwd=directory, capture_output=True, text=True)
报告会回显命中的密钥片段与命中行上下文到 stdout(--json 下 match 截断 50 字符、context 截断 100 字符;人类可读报告打印 match/context 前 80 字符),但脚本本身不写任何文件。
print(f" Match: {finding.matched_text[:80]}") print(f" Context: {finding.line_content[:80]}")
退出码即接口:有命中返回 1、干净返回 0,SKILL.md 明确把它当作 pre-commit 钩子用(阻断提交)。
sys.exit(1 if findings else 0)
2核心能力
3外部依赖
| Kind | Dependency / Endpoint |
|---|---|
| cli | repomix(第三方打包 CLI,需用户自行预装) |
| cli | python3(sys.executable 拉起同目录扫描器;仅标准库 os/re/sys/json/subprocess/pathlib) |
| package | repomix(npm 生态包,本 skill 只调用命令,不负责安装) |
4管控建议与判定依据
- scripts/: 执行本地脚本或命令: python3 scripts/safe_pack.py <directory> [--output file.xml] [--config repo
- 环境变量 / API Key: 读取厂商服务密钥: 按设计读取目标目录内文件文本做正则匹配,包含 .env/.env.example/.env.local/.env.production/.env.de
- 本地工作目录: 文件读写与工件输出: 两个脚本自身零文件写入(无 open(...,'w')、无临时文件、无日志文件)
5第二遍独立确认
- [ok] 外部依赖调用点反查(repomix / python3) — 全目录 grep subprocess 仅 3 处命中:safe_pack.py 的 import、L24 subprocess.run(调 scan_secrets.py,cmd 来自 cmd = [sys.executable, str(scan_script), …])、L74 subprocess.run(cmd, cwd=directory)(cmd 起点为 cmd = ['repomix'])。两个依赖条目均有真实调用点,无第三处。
- [ok] 网络外发反例查找 — grep requests|urllib|urlopen|socket|http|curl|wget 在两个脚本内零命中;SKILL.md 与 common_secrets.md 中的 https:// 全部是占位符(https://your-project-ref.supabase.co)、示例域名(.r2.cloudflarestorage.com 出现在正则 lookahead 文本中)或文档外链。无 DNS/HTTP 客户端、无遥测、无任何外发路径。
- [discrepancy] 扫描器返回码未被校验 → fail-open 反例 — run_secret_scan 只做 result = subprocess.run(...);随后 findings = json.loads(result.stdout) if result.stdout.strip() else [],从不检查 result.returncode。若扫描器因解释器缺失/脚本损坏/进程被杀而 stdout 为空,findings 被判为空列表,main 直接进入 '📦 Packing … with repomix' 分支——即『扫描失败』被当成『无密钥』。这与 SKILL.md『3. Blocks packaging if secrets found』的强承诺存在实现缺口:只承诺了『发现密钥时阻断』,未覆盖『扫描本身失败』。
- [discrepancy] SKILL.md 检测类型清单 vs SECRET_PATTERNS 实际键 — SKILL.md 列出 11 类(AWS Access Keys、R2 Account IDs/Access Keys、Supabase URL/Anon Keys、Stripe、OpenAI sk-、Google Gemini AIza、Generic API Keys、JWT、OAuth Client Secrets、Private Keys、Turnstile)。代码 SECRET_PATTERNS 实为 12 个键,但其中:无 R2 Access Keys 专用规则;无 Google Gemini(AIza…)规则(全目录 grep 'AIza' 零命中);OpenAI sk- 仅在同时出现 api_key/apikey 字样时可被 generic_api_key 捕获,stripe_key 只认 sk_live/sk_test 形式,因此裸的 sk-proj-… 赋值不一定会命中。文档检测类型清单宽于代码能力。
- [discrepancy] 假阳性抑制范围 vs 文档描述 — SKILL.md『Common False Positives』写的是跳过占位符、『Files matching .*test.*, .*example.*, .*sample.*』的文件与注释行。should_skip_match 实际对整行做 substring 包含判断(placeholder in line_lower),且注释判定用 ^\s*(?://|#|/\*|\*)——即抑制粒度是『行』而非『match』或『文件』:真密钥与 'example' 同行即被放过,被注释掉的真实凭证亦不上报。代码抑制面大于文档描述,属漏报向的偏差。
- [discrepancy] 扫描面缺口:不在白名单的高风险文件类型 — SCANNABLE_EXTENSIONS 只含 .ts/.tsx/.js/.jsx/.py/.md/.json/.yaml/.yml/.sh/.bash/.zsh/.sql/.go/.java/.rb/.php/.cs,SCANNABLE_FILENAMES 只补 7 个 dotenv 名。因此 *.pem、*.key、id_rsa、*.ppk、*.p12、*.crt、.envrc、*.toml/*.ini/*.conf/*.properties/*.xml 都不被扫描——private_key 规则(-----BEGIN … PRIVATE KEY-----)写好了却基本没有机会命中这些最常见的私钥载体。SKILL.md 未提示该覆盖边界(仅笼统说 may not catch all credential types)。
- [discrepancy] --force 语义与文档措辞一致性 — SKILL.md 标题写『**Force pack (dangerous, skip scan)**』,暗示跳过扫描;safe_pack.py 中 force = '--force' in sys.argv 之后仍照常执行 run_secret_scan 与 print_findings_report,只是跳过阻断并打印『WARNING: --force flag set, packing anyway despite secrets found!』。实际语义是『扫描照做、仅放行』,措辞『skip scan』不准确。
- [ok] 证据是否指向被扫文件内容/是否落盘 — 两脚本无 open(...,'w')、无 tempfile、无日志写入;匹配结果只经 print/ json.dumps 到 stdout(scan_secrets 的 to_dict 将 match 截断 50、context 截断 100;人类可读报告打印前 80 字符)。『不落盘、不外发』成立;但『不进日志』不成立——终端输出在 agent 会话中会被留存为上下文。
6结论
7847e6113e529f6b…