跳转到正文
报告库
用途分类 / 数据分析

Skill Creator Skill 安全审计

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

Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.

第三方安全检查结论

先别安装或运行

本次检查尚未完成,以下仅展示已取得的结果。

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

启动评审器会终止占用目标端口的任何进程

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

评审器在绑定默认端口前运行 `lsof -ti :<port>`,并对返回的每个 PID 发送 SIGTERM;它不检查该进程是否属于本 Skill。

为什么需要注意

如果端口 3117 已被开发服务器、数据库代理或其他工作占用,该进程会被意外停止,造成会话中断或未保存状态丢失。

启动非静态评审器时,它会先查找目标端口上的所有监听进程,并向每个 PID 发送 SIGTERM,没有确认进程归属。默认端口 3117 若正被其他本机应用使用,该应用可能被意外终止并丢失未保存工作。用户可改用 `--static`,或要求作者直接尝试空闲端口而不杀进程。

eval-viewer/generate_review.py:288来自代码打开原文件
def _kill_port(port: int) -> None:    """Kill any process listening on the given port."""    try:        result = subprocess.run(            ["lsof", "-ti", f":{port}"],            capture_output=True, text=True, timeout=5,        )        for pid_str in result.stdout.strip().split("\n"):            if pid_str.strip():                try:                    os.kill(int(pid_str.strip()), signal.SIGTERM)                except (ProcessLookupError, ValueError):                    pass        if result.stdout.strip():            time.sleep(0.5)    except subprocess.TimeoutExpired:
查看另外 2 个位置
eval-viewer/generate_review.py:387来自代码打开原文件
def main() -> None:    parser = argparse.ArgumentParser(description="Generate and serve eval review")    parser.add_argument("workspace", type=Path, help="Path to workspace directory")    parser.add_argument("--port", "-p", type=int, default=3117, help="Server port (default: 3117)")    parser.add_argument("--skill-name", "-n", type=str, default=None, help="Skill name for header")    parser.add_argument(
eval-viewer/generate_review.py:438来自代码打开原文件
    # Kill any existing process on the target port    port = args.port    _kill_port(port)    handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path)
会不会泄露文件和密钥?检查是否发送含密码或密钥的文件,以及代码里是否直接写了密钥。发现 2 项风险
高风险

恶意测试输出可向评审页面注入脚本并读取其他嵌入内容

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

评审器读取输出文件内容,用 `json.dumps` 生成数据后直接替换到 HTML 的 `<script>` 中,没有对 `</script>` 等脚本上下文终止序列做安全编码。测试产生的文件本身是不可信的。

为什么需要注意

特制输出在用户打开评审器时可作为浏览器脚本运行,读取页面中嵌入的其他测试输出、提示、评分和反馈,并可能通过网络发送这些数据或篡改评审结果。

评审器会读取测试输出,并把包含这些内容的 JSON 直接插入 HTML 的 `<script>`。`json.dumps` 不会默认转义 `</script>`;因此,若不可信输出含结束标签和脚本代码,浏览器打开评审页时可能执行它,并访问同页嵌入的其他输出、提示或反馈。可要求作者进行脚本上下文安全编码,或把数据放入非可执行节点并解析。

eval-viewer/generate_review.py:154来自代码打开原文件
    if ext in TEXT_EXTENSIONS:        try:            content = path.read_text(errors="replace")        except OSError:            content = "(Error reading file)"        return {            "name": path.name,            "type": "text",            "content": content,        }    elif ext in IMAGE_EXTENSIONS:
查看另外 2 个位置
eval-viewer/generate_review.py:270来自代码打开原文件
    embedded = {        "skill_name": skill_name,        "runs": runs,        "previous_feedback": previous_feedback,        "previous_outputs": previous_outputs,    }    if benchmark:        embedded["benchmark"] = benchmark    data_json = json.dumps(embedded)    return template.replace("/*__EMBEDDED_DATA__*/", f"const EMBEDDED_DATA = {data_json};")
eval-viewer/viewer.html:647来自说明文档打开原文件
  <script>    // ---- Embedded data (injected by generate_review.py) ----    /*__EMBEDDED_DATA__*/    // ---- State ----
中风险

打包步骤可能把技能目录中的凭据和私密文件一起发布

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

打包器递归加入所有文件,排除列表只包含缓存、node_modules、pyc、DS_Store 和根级 evals;没有排除 `.env`、私钥、令牌文件、版本控制元数据或其他秘密。符号链接指向的文件也未被显式拒绝。

为什么需要注意

如果用户或生成过程把凭据、客户数据或内部资料放在技能目录中,生成的 `.skill` 包可能将其交给安装者或分发对象。

打包器递归遍历技能目录中的所有文件,而排除项仅覆盖少数缓存、构建文件和根级 `evals`。因此,若用户选定的技能目录内含 `.env`、密钥、令牌、私密样本或 `.git` 内容,它们可能进入可分发的 `.skill` 压缩包;`is_file()` 也未明确拒绝符号链接。可要求作者在打包前列出清单、拒绝链接,并默认排除常见秘密和版本控制文件。

scripts/package_skill.py:19来自代码打开原文件
# Patterns to exclude when packaging skills.EXCLUDE_DIRS = {"__pycache__", "node_modules"}EXCLUDE_GLOBS = {"*.pyc"}EXCLUDE_FILES = {".DS_Store"}# Directories excluded only at the skill root (not when nested deeper).ROOT_EXCLUDE_DIRS = {"evals"}
查看另外 1 个位置
scripts/package_skill.py:89来自代码打开原文件
    # Create the .skill file (zip format)    try:        with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:            # Walk through the skill directory, excluding build artifacts            for file_path in skill_path.rglob('*'):                if not file_path.is_file():                    continue                arcname = file_path.relative_to(skill_path.parent)                if should_exclude(arcname):                    print(f"  Skipped: {arcname}")                    continue                zipf.write(file_path, arcname)                print(f"  Added: {arcname}")
会不会删除文件或一直在后台运行?检查是否大范围删除文件、改写磁盘,或设置自动启动。发现 1 项风险
低风险

触发测试会在实际项目中创建 `.claude/commands` 目录

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

测试程序从当前目录向上查找真实项目根目录,然后在那里创建 `.claude/commands` 和临时命令文件。虽然临时文件在 finally 中删除,但新建的目录不会删除。

为什么需要注意

测试会修改用户的项目结构,并可能留下空的 Claude 配置目录;崩溃或强制终止还可能留下临时命令文件,影响后续 Claude Code 技能发现。

触发测试会从当前目录向上寻找真实的 `.claude` 项目根,然后创建 `.claude/commands` 及临时命令文件。临时文件在 `finally` 中删除,但新建目录没有相应清理,因此测试可能永久改变项目目录结构。影响通常较小,但可能污染版本控制状态或覆盖用户对项目目录不应变更的预期。可要求使用隔离临时项目或删除空目录。

scripts/run_eval.py:22来自代码打开原文件
def find_project_root() -> Path:    """Find the project root by walking up from cwd looking for .claude/.    Mimics how Claude Code discovers its project root, so the command file    we create ends up where claude -p will look for it.    """    current = Path.cwd()    for parent in [current, *current.parents]:        if (parent / ".claude").is_dir():            return parent    return current
查看另外 2 个位置
scripts/run_eval.py:51来自代码打开原文件
    """    unique_id = uuid.uuid4().hex[:8]    clean_name = f"{skill_name}-skill-{unique_id}"    project_commands_dir = Path(project_root) / ".claude" / "commands"    command_file = project_commands_dir / f"{clean_name}.md"    try:        project_commands_dir.mkdir(parents=True, exist_ok=True)        # Use YAML block scalar to avoid breaking on quotes in description        indented_desc = "\n  ".join(skill_description.split("\n"))        command_content = (            f"---\n"            f"description: |\n"            f"  {indented_desc}\n"            f"---\n\n"            f"# {skill_name}\n\n"            f"This skill handles: {skill_description}\n"        )        command_file.write_text(command_content)
scripts/run_eval.py:179来自代码打开原文件
        return triggered    finally:        if command_file.exists():            command_file.unlink()
会不会绕过安全保护?检查是否跳过网站安全验证、开放过多文件权限,或取消操作前的确认。发现 1 项风险
中风险

任意网页可在本机评审服务运行时覆盖 feedback.json

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

本机服务的 POST 接口只检查路径和 JSON 中是否有 `reviews`,不验证 Origin、CSRF 令牌或请求者身份,然后直接写入反馈文件。

为什么需要注意

用户在评审期间访问恶意网页时,该网页可能向 localhost 发请求,清空或伪造反馈;后续技能修改可能依据被篡改的意见作出错误决定。

服务仅绑定本机,但 POST 处理器不验证 Origin、CSRF 令牌或 Content-Type;它只要求正文是含 `reviews` 的 JSON,随后覆盖工作区的 `feedback.json`。服务运行期间,恶意网页可用无需预检的请求发送 JSON 文本;即使网页不能读取响应,也可能篡改用户用于后续技能决策的反馈。可要求加入随机令牌、Origin 校验及严格内容类型检查。

eval-viewer/generate_review.py:361来自代码打开原文件
    def do_POST(self) -> None:        if self.path == "/api/feedback":            length = int(self.headers.get("Content-Length", 0))            body = self.rfile.read(length)            try:                data = json.loads(body)                if not isinstance(data, dict) or "reviews" not in data:                    raise ValueError("Expected JSON object with 'reviews' key")                self.feedback_path.write_text(json.dumps(data, indent=2) + "\n")                resp = b'{"ok":true}'                self.send_response(200)            except (json.JSONDecodeError, OSError, ValueError) as e:                resp = json.dumps({"error": str(e)}).encode()                self.send_response(500)            self.send_header("Content-Type", "application/json")            self.send_header("Content-Length", str(len(resp)))            self.end_headers()            self.wfile.write(resp)        else:
查看另外 2 个位置
eval-viewer/generate_review.py:441来自代码打开原文件
    _kill_port(port)    handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path)    try:        server = HTTPServer(("127.0.0.1", port), handler)    except OSError:
eval-viewer/generate_review.py:438来自代码打开原文件
    # Kill any existing process on the target port    port = args.port    _kill_port(port)    handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path)    try:        server = HTTPServer(("127.0.0.1", port), handler)    except OSError:        # Port still in use after kill attempt — find a free one        server = HTTPServer(("127.0.0.1", 0), handler)        port = server.server_address[1]
会不会误导 AI 或隐藏内容?检查工作说明是否要求 AI 忽略你的指令、干扰检查结果,或夹带看不见的文字。未发现风险
会不会偷偷改推广链接或收款方?检查是否强制替换推广链接或收款对象,同时要求隐瞒更改。发现 1 项风险
中风险

优化循环绕过嵌套会话保护并大量使用当前账号的 Claude 认证

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

代码明确删除 `CLAUDECODE` 环境变量以允许嵌套 `claude -p`,并使用现有会话认证。默认配置对每个查询运行三次、最多五轮;文档建议约 20 个查询,因此一次优化可触发数百次账号调用。

为什么需要注意

这可能快速消耗付费额度、速率限制或组织配额,并把测试查询和完整技能内容提交给模型服务。

优化功能明确复用当前 Claude Code 登录状态,删除 `CLAUDECODE` 防护变量后启动嵌套 `claude -p`。文档建议 20 条查询;默认每条运行三次、最多五轮,即仅触发评测就可能达到约 300 次模型调用,另有改写调用。这属于所述优化功能,但可能消耗账号配额、费用和时间。用户可要求先确认调用预算,并降低查询数、轮数或重复次数。

scripts/improve_description.py:4来自代码打开原文件
Takes eval results (from run_eval.py) and generates an improved descriptionby calling `claude -p` as a subprocess (same auth pattern as run_eval.py —uses the session's Claude Code auth, no separate ANTHROPIC_API_KEY needed)."""
查看另外 4 个位置
scripts/run_eval.py:80来自代码打开原文件
        # Remove CLAUDECODE env var to allow nesting claude -p inside a        # Claude Code session. The guard is for interactive terminal conflicts;        # programmatic subprocess usage is safe.        env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}        process = subprocess.Popen(            cmd,            stdout=subprocess.PIPE,            stderr=subprocess.DEVNULL,            cwd=project_root,            env=env,        )
scripts/run_loop.py:249来自代码打开原文件
    parser.add_argument("--description", default=None, help="Override starting description")    parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")    parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")    parser.add_argument("--max-iterations", type=int, default=5, help="Max improvement iterations")    parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")    parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")    parser.add_argument("--holdout", type=float, default=0.4, help="Fraction of eval set to hold out for testing (0 to disable)")    parser.add_argument("--model", required=True, help="Model for improvement")    parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
SKILL.md:339来自说明文档打开原文件
Create 20 eval queries — a mix of should-trigger and should-not-trigger. Save as JSON:```json[  {"query": "the user prompt", "should_trigger": true},  {"query": "another prompt", "should_trigger": false}]```
scripts/improve_description.py:26来自代码打开原文件
    """    cmd = ["claude", "-p", "--output-format", "text"]    if model:        cmd.extend(["--model", model])    # Remove CLAUDECODE env var to allow nesting claude -p inside a    # Claude Code session. The guard is for interactive terminal conflicts;    # programmatic subprocess usage is safe. Same pattern as run_eval.py.    env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}    result = subprocess.run(        cmd,        input=prompt,        capture_output=True,        text=True,        env=env,        timeout=timeout,

Skill 逻辑拆解

8 个说明模块

该 Skill 会创建或修改技能,生成测试用例,并比较“使用技能”和基线运行的结果;完成后还可打包整个技能目录。

查看原文
SKILL.md:12来自说明文档打开原文件
- Decide what you want the skill to do and roughly how it should do it- Write a draft of the skill- Create a few test prompts and run claude-with-access-to-the-skill on them- Help the user evaluate the results both qualitatively and quantitatively  - While the runs happen in the background, draft some quantitative evals if there aren't any (if there are some, you can either use as is or modify if you feel something needs to change about them). Then explain them to the user (or if they already existed, explain the ones that already exist)  - Use the `eval-viewer/generate_review.py` script to show the user the results for them to look at, and also let them look at the quantitative metrics- Rewrite the skill based on feedback from the user's evaluation of the results (and also if there are any glaring flaws that become apparent from the quantitative benchmarks)- Repeat until you're satisfied- Expand the test set and try again at larger scale
SKILL.md:408来自说明文档打开原文件
### Package and Present (only if `present_files` tool is available)Check whether you have access to the `present_files` tool. If you don't, skip this step. If you do, package the skill and present the .skill file to the user:```bashpython -m scripts.package_skill <path/to/skill-folder>```After packaging, direct the user to the resulting `.skill` file path so they can install it.

评审器会递归寻找包含 `outputs/` 的运行目录,把其中的文本和二进制输出嵌入一个 HTML 页面,并在默认情况下通过本机 HTTP 服务展示。

查看原文
eval-viewer/generate_review.py:60来自代码打开原文件
def find_runs(workspace: Path) -> list[dict]:    """Recursively find directories that contain an outputs/ subdirectory."""    runs: list[dict] = []    _find_runs_recursive(workspace, workspace, runs)    runs.sort(key=lambda r: (r.get("eval_id", float("inf")), r["id"]))    return runs
eval-viewer/generate_review.py:149来自代码打开原文件
def embed_file(path: Path) -> dict:    """Read a file and return an embedded representation."""    ext = path.suffix.lower()    mime = get_mime_type(path)    if ext in TEXT_EXTENSIONS:        try:            content = path.read_text(errors="replace")        except OSError:            content = "(Error reading file)"        return {            "name": path.name,            "type": "text",            "content": content,        }    elif ext in IMAGE_EXTENSIONS:
eval-viewer/generate_review.py:443来自代码打开原文件
    try:        server = HTTPServer(("127.0.0.1", port), handler)    except OSError:        # Port still in use after kill attempt — find a free one        server = HTTPServer(("127.0.0.1", 0), handler)        port = server.server_address[1]    url = f"http://localhost:{port}"    print(f"\n  Eval Viewer")

打包器会遍历指定技能目录,将除少数固定模式外的所有文件写入 `.skill` ZIP 包。

查看原文
scripts/package_skill.py:19来自代码打开原文件
# Patterns to exclude when packaging skills.EXCLUDE_DIRS = {"__pycache__", "node_modules"}EXCLUDE_GLOBS = {"*.pyc"}EXCLUDE_FILES = {".DS_Store"}# Directories excluded only at the skill root (not when nested deeper).ROOT_EXCLUDE_DIRS = {"evals"}
scripts/package_skill.py:89来自代码打开原文件
    # Create the .skill file (zip format)    try:        with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:            # Walk through the skill directory, excluding build artifacts            for file_path in skill_path.rglob('*'):                if not file_path.is_file():                    continue                arcname = file_path.relative_to(skill_path.parent)                if should_exclude(arcname):                    print(f"  Skipped: {arcname}")                    continue                zipf.write(file_path, arcname)                print(f"  Added: {arcname}")
从这里开始 · 工作说明SKILL.md
skill-creator
连线表示工作说明包含的模块,不是实际运行顺序。点击模块可查看原文。 另有 1 个章节,可在原文件中查看。

文件引用关系图

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

检查范围与遗漏

  • 有检测结果未通过证据校验或未完成处理,本报告不能代表完整检查。
逐文件查看涉及的内容

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

  • SKILL.md已纳入全文
  • scripts/__init__.py已纳入全文
  • scripts/aggregate_benchmark.py已纳入全文
  • scripts/generate_report.py已纳入全文
  • scripts/improve_description.py已纳入全文
  • scripts/package_skill.py已纳入全文
  • scripts/quick_validate.py已纳入全文
  • scripts/run_eval.py已纳入全文
  • scripts/run_loop.py已纳入全文
  • scripts/utils.py已纳入全文
  • assets/eval_review.html已纳入全文
  • references/schemas.md已纳入全文
  • eval-viewer/generate_review.py已纳入全文
  • agents/analyzer.md已纳入全文
  • agents/comparator.md已纳入全文
  • agents/grader.md已纳入全文
  • eval-viewer/viewer.html已纳入全文
  • LICENSE.txt已纳入全文

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

  • LICENSE.txt许可说明
  • SKILL.md工作说明
  • agents/analyzer.md配套文件
  • agents/comparator.md配套文件
  • agents/grader.md配套文件
  • assets/eval_review.html配套文件
  • eval-viewer/generate_review.py脚本
  • eval-viewer/viewer.html配套文件
  • references/schemas.md配套文件
  • scripts/__init__.py脚本
  • scripts/aggregate_benchmark.py脚本
  • scripts/generate_report.py脚本
  • scripts/improve_description.py脚本
  • scripts/package_skill.py脚本
  • scripts/quick_validate.py脚本
  • scripts/run_eval.py脚本
  • scripts/run_loop.py脚本
  • scripts/utils.py脚本

代码和说明中提到的操作

连接外部网站
eval-viewer/generate_review.py:449来自代码打开原文件
    url = f"http://localhost:{port}"    print(f"\n  Eval Viewer")
scripts/generate_report.py:39来自代码打开原文件
""" + refresh_tag + """    <title>""" + title_prefix + """Skill Description Optimization</title>    <link rel="preconnect" href="https://fonts.googleapis.com">    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
scripts/generate_report.py:40来自代码打开原文件
    <link rel="preconnect" href="https://fonts.googleapis.com">    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;600&family=Lora:wght@400;500&display=swap" rel="stylesheet">
运行命令
eval-viewer/generate_review.py:22来自代码打开原文件
import signalimport subprocessimport sys
eval-viewer/generate_review.py:291来自代码打开原文件
    try:        result = subprocess.run(            ["lsof", "-ti", f":{port}"],
eval-viewer/generate_review.py:303来自代码打开原文件
            time.sleep(0.5)    except subprocess.TimeoutExpired:        pass
读取文件
eval-viewer/generate_review.py:94来自代码打开原文件
            try:                metadata = json.loads(candidate.read_text())                prompt = metadata.get("prompt", "")
eval-viewer/generate_review.py:107来自代码打开原文件
                try:                    text = candidate.read_text()                    match = re.search(r"## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)", text)
eval-viewer/generate_review.py:134来自代码打开原文件
            try:                grading = json.loads(candidate.read_text())            except (json.JSONDecodeError, OSError):
修改文件
eval-viewer/generate_review.py:348来自代码打开原文件
            self.end_headers()            self.wfile.write(content)        elif self.path == "/api/feedback":
eval-viewer/generate_review.py:357来自代码打开原文件
            self.end_headers()            self.wfile.write(data)        else:
eval-viewer/generate_review.py:369来自代码打开原文件
                    raise ValueError("Expected JSON object with 'reviews' key")                self.feedback_path.write_text(json.dumps(data, indent=2) + "\n")                resp = b'{"ok":true}'
读取密钥或账号配置
scripts/improve_description.py:6来自代码打开原文件
by calling `claude -p` as a subprocess (same auth pattern as run_eval.py —uses the session's Claude Code auth, no separate ANTHROPIC_API_KEY needed)."""
scripts/improve_description.py:33来自代码打开原文件
    # programmatic subprocess usage is safe. Same pattern as run_eval.py.    env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
scripts/run_eval.py:83来自代码打开原文件
        # programmatic subprocess usage is safe.        env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
读取了多少行
5,672
文件校验值(用于核对版本)
25871608bc6d1204fd6c5e73d619ba73f736ec0ff8ec145536ff375a697c5f39