跳转到正文
报告库
用途分类 / 其他用途

Caveman Compress Skill 安全审计

作者说它能做什么(原文)

>

第三方安全检查结论

先别安装或运行

已检查文件
10
发现的风险
4
会不会运行危险命令?检查是否下载程序后直接运行、让他人远程控制电脑,或藏起要运行的命令。发现 1 项风险
中风险

未设置 API 密钥时会执行 PATH 中解析到的 claude 程序

原文依据:4 处
发现了什么

回退分支使用 shutil.which("claude") 查找可执行文件并直接运行。虽然参数列表固定且未使用 shell,但它仍信任当前 PATH 对程序身份的解析。

为什么需要注意

如果 PATH 被不可信安装项、项目环境或被攻陷的软件抢先放入同名程序,该程序会以运行 Skill 的用户权限执行,并收到压缩提示中的文件内容。

支持,但风险有条件。未设置 ANTHROPIC_API_KEY 时,技能通过当前 PATH 解析名为 claude 的程序并执行它;若 PATH 被其他软件或低信任目录污染,错误的同名程序可获得完整提示内容及该进程已有权限。固定参数列表且未启用 shell,降低了文件内容造成命令注入的风险,但不验证可执行文件身份。用户可限制 PATH、核对 claude 的实际位置,或要求作者固定并验证可信程序路径。

scripts/compress.py:413来自代码打开原文件
    """    api_key = os.environ.get("ANTHROPIC_API_KEY")    if api_key:        try:            import anthropic            client = anthropic.Anthropic(api_key=api_key, timeout=CLAUDE_CALL_TIMEOUT_SECONDS)            msg = client.messages.create(                model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),                max_tokens=8192,                messages=[{"role": "user", "content": prompt}],            )            # Tool-heavy models can put a tool_use or thinking block first; take            # the first text block instead of trusting content[0].            text = next((block.text for block in msg.content if getattr(block, "type", None) == "text"), "")            return strip_llm_wrapper(text.strip())        except ImportError:            pass  # anthropic not installed, fall back to CLI    # Fallback: use claude CLI (handles desktop auth).
查看另外 3 个位置
scripts/compress.py:430来自代码打开原文件
            pass  # anthropic not installed, fall back to CLI    # Fallback: use claude CLI (handles desktop auth).    # Resolve binary via shutil.which so Windows .cmd/.bat shims (e.g.    # %APPDATA%\npm\claude.CMD) work without shell=True. On POSIX,    # shutil.which returns the same absolute path as the implicit lookup,    # so this is a no-op there. Falls back to bare "claude" if not found    # on PATH so subprocess raises a clear FileNotFoundError.    claude_bin = shutil.which("claude") or "claude"    try:        result = subprocess.run(            [                claude_bin,                "--print",                "--setting-sources",                "",                "--strict-mcp-config",            ],            input=prompt,            text=True,
scripts/compress.py:403来自代码打开原文件
    Prefers the Anthropic SDK when ANTHROPIC_API_KEY is set; otherwise falls    back to the ``claude --print`` CLI (which handles desktop auth).
scripts/compress.py:446来自代码打开原文件
            ],            input=prompt,            text=True,            capture_output=True,            check=True,            encoding="utf-8",            errors="replace",            timeout=CLAUDE_CALL_TIMEOUT_SECONDS,        )
会不会泄露文件和密钥?检查是否发送含密码或密钥的文件,以及代码里是否直接写了密钥。发现 2 项风险
高风险

目标文件内容会发送给 Anthropic;修复阶段可能发送完整原文

原文依据:6 处
发现了什么

初次请求发送代码块之外的正文。若结构验证失败,修复提示会嵌入完整 original_text,包括此前被遮蔽的代码块和 YAML 前置内容。敏感检查只看文件名和路径,因此普通名称文件中的令牌、客户信息、内部指令或私有代码仍可能被发送。

为什么需要注意

文件内容将跨越本机边界,进入 Anthropic SDK 或 Claude CLI 使用的服务和账户;这可能违反组织的数据驻留、保密或第三方处理政策。

支持。首次压缩会把目标正文放入提示并发送给 Anthropic SDK或 Claude CLI;代码块虽先被遮蔽,但验证失败后的修复提示直接包含完整 original_text,因此 YAML、代码块和其他原文也会被发送。拒绝机制仅依据路径和文件名特征,普通名称文件中的密钥、客户资料或私有代码不会因此被识别。用户应仅提交可向 Anthropic 披露的文件,并要求作者增加内容级扫描或明确确认。

scripts/compress.py:403来自代码打开原文件
    Prefers the Anthropic SDK when ANTHROPIC_API_KEY is set; otherwise falls    back to the ``claude --print`` CLI (which handles desktop auth).    On Windows the CLI subprocess decoding defaults to the system codepage    (cp1251 / cp1252) and crashes on UTF-8 output — see issue #152. Pinning    ``encoding="utf-8"`` with ``errors="replace"`` matches the CLI's actual    native I/O and prevents the UnicodeDecodeError before validation can    report. Windows users with non-ASCII content can also set    ``ANTHROPIC_API_KEY`` to route through the SDK and skip the subprocess.    """    api_key = os.environ.get("ANTHROPIC_API_KEY")    if api_key:        try:            import anthropic            client = anthropic.Anthropic(api_key=api_key, timeout=CLAUDE_CALL_TIMEOUT_SECONDS)            msg = client.messages.create(                model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),                max_tokens=8192,                messages=[{"role": "user", "content": prompt}],            )            # Tool-heavy models can put a tool_use or thinking block first; take
查看另外 5 个位置
scripts/compress.py:601来自代码打开原文件
    # Refuse files that look like they contain secrets or PII. Compressing ships    # the raw bytes to the Anthropic API — a third-party boundary — so we fail    # loudly rather than silently exfiltrate credentials or keys. Override is    # intentional: the user must rename the file if the heuristic is wrong.    if is_sensitive_path(filepath):        raise ValueError(            f"Refusing to compress {filepath}: filename looks sensitive "            "(credentials, keys, secrets, or known private paths). "            "Compression sends file contents to the Anthropic API. "            "Rename the file if this is a false positive."        )
scripts/compress.py:503来自代码打开原文件
ORIGINAL (reference only):{original}COMPRESSED (fix this):{compressed}
scripts/compress.py:730来自代码打开原文件
        print("Fixing with Claude...")        fixed = call_claude(            build_fix_prompt(original_text, compressed, result.errors)        )
scripts/compress.py:413来自代码打开原文件
    """    api_key = os.environ.get("ANTHROPIC_API_KEY")    if api_key:        try:            import anthropic            client = anthropic.Anthropic(api_key=api_key, timeout=CLAUDE_CALL_TIMEOUT_SECONDS)            msg = client.messages.create(                model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),                max_tokens=8192,                messages=[{"role": "user", "content": prompt}],            )            # Tool-heavy models can put a tool_use or thinking block first; take
scripts/compress.py:654来自代码打开原文件
    # Step 1: Compress (body only, frontmatter excluded)    print("Compressing with Claude...")    masked_body, code_blocks = mask_code_blocks(body)    masked_compressed = call_claude(build_compress_prompt(masked_body))    try:
中风险

原始文件的完整副本会长期保存在项目目录之外

原文依据:4 处
发现了什么

成功流程把原始字节写入 XDG/用户本地数据目录下的备份路径。该副本不跟随项目的删除、归档、访问控制检查或秘密扫描流程;SECURITY.md 同时声称不会访问用户所给路径之外的文件,与实际备份和锁文件行为不一致。

为什么需要注意

即使项目副本后来被清理,旧指令、个人信息或内部资料仍可能留在用户数据目录中,并被本机备份、同步或取证工具继续收集。矛盾的说明也可能使用户低估此副本。

支持。成功流程会把原始字节写到用户数据目录中的项目外备份;代码没有到期或自动清理机制,因此副本会持续存在,且项目内的删除、归档或扫描通常不会涵盖它。备份目录只使用父目录名、文件名使用 stem,可能跨不同同名目录产生冲突并中止后续运行。SECURITY.md 的“不访问用户提供路径之外文件”说法也与备份及锁文件实现不符。用户可限制数据目录权限,并要求作者说明保留期、清理方式和更强的路径隔离。

scripts/compress.py:93来自代码打开原文件
def _state_base_dir(kind: str) -> Path:    """Shared platform-aware base dir for caveman-compress state (backups, locks) — Windows uses %LOCALAPPDATA%, else $XDG_DATA_HOME or ~/.local/share."""    if _IS_WINDOWS:        local_appdata = os.environ.get("LOCALAPPDATA")        base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"    else:        xdg = os.environ.get("XDG_DATA_HOME")        base = Path(xdg) if xdg else Path.home() / ".local" / "share"    return base / "caveman-compress" / kinddef backup_dir_for(filepath: Path) -> Path:    """Out-of-tree backup dir for filepath, keyed by its parent dir name — kept outside the source tree so skill auto-loaders don't re-ingest `.original.md` backups as live files."""    return _state_base_dir("backups") / filepath.parent.name
查看另外 3 个位置
scripts/compress.py:691来自代码打开原文件
    # Save original as backup, then verify the backup readback before    # touching the input file. If the filesystem dropped bytes (encoding,    # antivirus, disk full), unlink the bad backup and abort instead of    # leaving the user with a corrupt backup + compressed primary.    backup_dir.mkdir(parents=True, exist_ok=True)    write_bytes_atomic(backup_path, original_raw)    if backup_path.read_bytes() != original_raw:        print(f"❌ Backup write verification failed: {backup_path}")
SECURITY.md:15来自说明文档打开原文件
- 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
scripts/compress.py:625来自代码打开原文件
    original_text, newline, original_raw = read_source(filepath)    # Store backup outside the source directory so skill auto-loaders don't    # re-ingest the `.original.md` copy as a live file. Mirror the source's    # parent-dir name + stem under a platform-aware base to reduce collisions.    backup_dir = backup_dir_for(filepath)    backup_path = backup_dir / (filepath.stem + ".original.md")
会不会删除文件或一直在后台运行?检查是否大范围删除文件、改写磁盘,或设置自动启动。未发现风险
会不会绕过安全保护?检查是否跳过网站安全验证、开放过多文件权限,或取消操作前的确认。未发现风险
会不会误导 AI 或隐藏内容?检查工作说明是否要求 AI 忽略你的指令、干扰检查结果,或夹带看不见的文字。发现 1 项风险
高风险

结构验证通过后仍可能静默改变 CLAUDE.md 等长期指令的含义

原文依据:5 处
发现了什么

压缩明确允许删除措辞、合并项目和减少示例。验证器只检查标题、代码块、URL、部分路径、项目符号数量和行内代码;它不验证语义、否定词、优先级、表格内容、普通文本命令、数字或技术约束是否等价。目标随后会被原地覆盖。文件中的提示注入文本也可影响模型产生的替换内容。

为什么需要注意

安全限制、部署规则、待办事项或用户偏好可能被弱化、反转或删除,同时工具报告验证成功。若文件会被代理自动加载,变化还会影响以后会话中的操作和决策。备份可帮助恢复,但不会阻止错误指令先被使用。

支持。该技能允许合并条目、删减示例和改写自然语言,但成功条件只验证标题、代码块、URL、路径、项目符号及行内代码,不检查普通文字的语义、否定、优先级或表格内容。通过这些结构检查后,模型输出会覆盖原文件;对 CLAUDE.md 之类长期指令,这可能静默改变后续决策。原文也直接嵌入模型提示,因此其中伪装成指令的文字可能影响输出。备份可用于恢复,但不能防止语义漂移。

SKILL.md:64来自说明文档打开原文件
### Compress- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"- Fragments OK: "Run tests before commit" not "You should always run tests before committing"- Drop "you should", "make sure to", "remember to" — just state the action- Merge redundant bullets that say the same thing differently- Keep one example where multiple examples show the same pattern
查看另外 4 个位置
scripts/validate.py:382来自代码打开原文件
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:    result = ValidationResult()    orig = read_file(original_path)    comp = read_file(compressed_path)    validate_headings(orig, comp, result)    validate_code_blocks(orig, comp, result)    validate_urls(orig, comp, result)    validate_paths(orig, comp, result)    validate_bullets(orig, comp, result)    validate_inline_codes(orig, comp, result)    return result
scripts/compress.py:714来自代码打开原文件
        if result.is_valid:            print("Validation passed")            _write_target(filepath, compressed, backup_path, newline)            staging_path.unlink(missing_ok=True)            return True
README.md:162来自说明文档打开原文件
Caveman reduced counted tokens by about 46% on five listed fixtures. Validatorsconfirmed headings, code blocks, URLs, and file paths. They did not establishgeneral semantic or task-quality equivalence.
scripts/compress.py:464来自代码打开原文件
def build_compress_prompt(original: str) -> str:    return f"""Compress this markdown into caveman format.STRICT RULES:- Do NOT modify anything inside ``` code blocks- Do NOT modify anything inside a 4-space-indented code block either — those are code too, and they are validated- Do NOT modify anything inside inline backticks- Preserve ALL URLs exactly- Preserve ALL headings exactly- Preserve file paths and commands- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.Only compress natural language.TEXT:{original}"""
会不会偷偷改推广链接或收款方?检查是否强制替换推广链接或收款对象,同时要求隐瞒更改。未发现风险

Skill 逻辑拆解

6 个说明模块

该 Skill 只接受被判定为自然语言的文件;常见代码/配置扩展名、备份文件以及名称或路径看似包含密钥的文件会被拒绝。该检查主要基于扩展名和路径名称,而不是扫描文件内容。

查看原文
scripts/detect.py:119来自代码打开原文件
def should_compress(filepath: Path) -> bool:    """Return True if the file is natural language and should be compressed."""    if not filepath.is_file():        return False    # Skip backup files    if filepath.name.endswith(".original.md"):        return False    return detect_file_type(filepath) == "natural_language"
scripts/compress.py:197来自代码打开原文件
def is_sensitive_path(filepath: Path) -> bool:    """Heuristic denylist for files that must never be shipped to a third-party API."""    name = filepath.name    if SENSITIVE_BASENAME_REGEX.match(name):        return True    # Normalize every component, not only basename: directories named    # `api-keys`, `private_keys`, or singular `secret` are equally sensitive.    normalized_parts = {        re.sub(r"[_\-\s.]", "", part.lower()) for part in filepath.parts    }    if normalized_parts & SENSITIVE_PATH_COMPONENTS:        return True    return any(        token in part        for part in normalized_parts        for token in SENSITIVE_NAME_TOKENS    )

正文中的代码块会先替换为标记,再把其余正文交给 Claude 压缩;若验证失败,修复请求会同时包含完整原文和压缩稿。

查看原文
scripts/compress.py:654来自代码打开原文件
    # Step 1: Compress (body only, frontmatter excluded)    print("Compressing with Claude...")    masked_body, code_blocks = mask_code_blocks(body)    masked_compressed = call_claude(build_compress_prompt(masked_body))    try:        compressed_body = restore_code_blocks(masked_compressed, code_blocks)    except ValueError as error:
scripts/compress.py:503来自代码打开原文件
ORIGINAL (reference only):{original}COMPRESSED (fix this):{compressed}
scripts/compress.py:730来自代码打开原文件
        print("Fixing with Claude...")        fixed = call_claude(            build_fix_prompt(original_text, compressed, result.errors)        )

成功时,原文件会被压缩结果原地替换,原始字节则写入用户数据目录下的持久备份。写入采用临时文件和原子替换。

查看原文
scripts/compress.py:93来自代码打开原文件
def _state_base_dir(kind: str) -> Path:    """Shared platform-aware base dir for caveman-compress state (backups, locks) — Windows uses %LOCALAPPDATA%, else $XDG_DATA_HOME or ~/.local/share."""    if _IS_WINDOWS:        local_appdata = os.environ.get("LOCALAPPDATA")        base = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"    else:        xdg = os.environ.get("XDG_DATA_HOME")        base = Path(xdg) if xdg else Path.home() / ".local" / "share"    return base / "caveman-compress" / kinddef backup_dir_for(filepath: Path) -> Path:    """Out-of-tree backup dir for filepath, keyed by its parent dir name — kept outside the source tree so skill auto-loaders don't re-ingest `.original.md` backups as live files."""    return _state_base_dir("backups") / filepath.parent.name
scripts/compress.py:695来自代码打开原文件
    # leaving the user with a corrupt backup + compressed primary.    backup_dir.mkdir(parents=True, exist_ok=True)    write_bytes_atomic(backup_path, original_raw)    if backup_path.read_bytes() != original_raw:        print(f"❌ Backup write verification failed: {backup_path}")
scripts/compress.py:714来自代码打开原文件
        if result.is_valid:            print("Validation passed")            _write_target(filepath, compressed, backup_path, newline)            staging_path.unlink(missing_ok=True)            return True
从这里开始 · 工作说明SKILL.md
caveman-compress
连线表示工作说明包含的模块,不是实际运行顺序。点击模块可查看原文。

文件引用关系图

2 处引用
哪些文件发起引用引用了什么
连线表示真实的文件引用,不是运行顺序。点击节点可高亮相关连线,并查看具体文件和原文位置。虚线表示还有文件需要定位。
文件与检查记录10 个文件

检查范围与遗漏

逐文件查看涉及的内容

下方列出本次涉及的原文范围;纳入检查不代表已查清所有问题。

  • SKILL.md已纳入全文
  • scripts/__init__.py已纳入全文
  • scripts/__main__.py已纳入全文
  • scripts/benchmark.py已纳入全文
  • scripts/cli.py已纳入全文
  • scripts/compress.py已纳入全文
  • scripts/detect.py已纳入全文
  • scripts/validate.py已纳入全文
  • SECURITY.md已纳入全文
  • README.md已纳入全文

这份报告只针对上方版本。我们看了拿到的代码和说明文件,没有实际运行 Skill,也没有检查它另外安装的软件包。因此,这不是“保证安全”的承诺;换了版本或使用环境,结果也可能不同。

  • README.md配套文件
  • SECURITY.md配套文件
  • SKILL.md工作说明
  • scripts/__init__.py脚本
  • scripts/__main__.py脚本
  • scripts/benchmark.py脚本
  • scripts/cli.py脚本
  • scripts/compress.py脚本
  • scripts/detect.py脚本
  • scripts/validate.py脚本

代码和说明中提到的操作

连接外部网站
README.md:2来自说明文档打开原文件
<p align="center">  <img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="80" /></p>
README.md:177来自说明文档打开原文件
This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit.
读取文件
scripts/benchmark.py:26来自代码打开原文件
def benchmark_pair(orig_path: Path, comp_path: Path):    orig_text = orig_path.read_text(encoding="utf-8", errors="ignore")    comp_text = comp_path.read_text(encoding="utf-8", errors="ignore")
scripts/benchmark.py:27来自代码打开原文件
    orig_text = orig_path.read_text(encoding="utf-8", errors="ignore")    comp_text = comp_path.read_text(encoding="utf-8", errors="ignore")
scripts/compress.py:162来自代码打开原文件
        raise OSError(f"Refusing to open lock file through a symlink: {lock_path}")    fd = os.open(lock_path, os.O_CREAT | os.O_RDWR | _O_NOFOLLOW, 0o600)    try:
运行命令
scripts/compress.py:16来自代码打开原文件
import statimport subprocessimport sys
scripts/compress.py:223来自代码打开原文件
    STARTS and ENDS with a fence line. An ordinary README section —    ```bash npm install``` , prose, ```bash npm test``` — came back with its    first and last fence markers deleted and its two code blocks merged into
scripts/compress.py:406来自代码打开原文件
    On Windows the CLI subprocess decoding defaults to the system codepage    (cp1251 / cp1252) and crashes on UTF-8 output — see issue #152. Pinning
安装其他软件包
scripts/compress.py:223来自代码打开原文件
    STARTS and ENDS with a fence line. An ordinary README section —    ```bash npm install``` , prose, ```bash npm test``` — came back with its    first and last fence markers deleted and its two code blocks merged into
README.md:151来自说明文档打开原文件
- File paths (`/src/components/...`)- Commands (`npm install`, `git commit`)- Technical terms, library names, API names
SKILL.md:51来自说明文档打开原文件
- File paths (`/src/components/...`, `./config.yaml`)- Commands (`npm install`, `git commit`, `docker build`)- Technical terms (library names, API names, protocols, algorithms)
读取密钥或账号配置
scripts/compress.py:65来自代码打开原文件
# them ships raw bytes to the Anthropic API — a third-party data boundary that# developers on sensitive codebases cannot cross. detect.py already skips .env# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
scripts/compress.py:66来自代码打开原文件
# developers on sensitive codebases cannot cross. detect.py already skips .env# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would# slip through the natural-language filter. This is a hard refuse before read.
scripts/compress.py:70来自代码打开原文件
    r"(?ix)^("    r"\.env(\..+)?"    r"|\.netrc"
修改文件
scripts/compress.py:165来自代码打开原文件
        if os.fstat(fd).st_size == 0:            os.write(fd, b"\0")  # msvcrt.locking needs at least one byte in the file to lock        os.lseek(fd, 0, 0)
scripts/compress.py:254来自代码打开原文件
def write_text_atomic(path: Path, text: str, newline: str = "\n") -> None:    """Write ``text`` to ``path`` atomically as UTF-8.
scripts/compress.py:257来自代码打开原文件
    Path.write_text() truncates the destination before encoding the string —    a UnicodeEncodeError (or any other failure) partway through leaves a
读取了多少行
1,834
文件校验值(用于核对版本)
1a5de1df97be5922d1adcea52955e45164a60665ae89456add6621531c522ec4