安装第三方 Skill 前,先确认它会读取什么、写入哪里、执行哪些命令、访问哪些域名;只要其中一项说不清,就不要直接放进真实项目运行。

你看到一个 Skill 能自动修 Bug、生成测试或审计代码,最容易被“装上就能用”吸引。但 Skill 不是普通提示词,它可能同时带有脚本、依赖、工具调用和操作说明。功能描述写得漂亮,不代表执行边界安全。本文以一个可复现的目录扫描器为例,带你完成文件清点、静态检查、隔离试跑和安装决策。适用范围是支持读取本地技能目录、执行脚本或调用工具的 AI 编程代理。
一、先看结论:安全审查四步
| 步骤 | 动作 | 目标 | 输出证据 |
|---|---|---|---|
| 1 | 画清权限边界 | 确认读、写、执行、网络范围 | 四问答案 |
| 2 | 按顺序读四类文件 | 理解触发条件与高影响行为 | 文件清单与风险笔记 |
| 3 | 只读扫描器筛查 | 防止漏看高风险动作 | 扫描报告与退出码 |
| 4 | 隔离副本试跑 | 验证实际文件、命令、网络变化 | 基线对比与试跑日志 |
| 5 | 做安装决策 | 安装、收紧或拒绝 | 决策依据与审批记录 |
一句话:功能说明只能解释目的,脚本和工具调用才能证明行为;证据不足就收紧或拒绝。
二、先画清 Skill 的权限边界
第三方 Skill 是一组会影响代理行为的文件,而不是一段只能被阅读的说明文字。它通常包含主说明、辅助文档、脚本、配置和依赖清单。代理一旦按这些文件行动,就可能读取仓库内容、启动命令、创建文件或发起网络请求。
安全审查的第一步不是找“恶意关键字”,而是回答四个问题:
| 范围 | 要问的问题 | 高风险信号 |
|---|---|---|
| 读范围 | 只读当前项目,还是访问用户目录、SSH、云凭证、环境变量? | 枚举环境变量、读取 .ssh、.aws、.env |
| 写范围 | 只生成报告,还是改源码、锁文件、Git 配置甚至系统目录? | 递归删除、修改锁文件、写入项目外路径 |
| 执行范围 | 调用固定命令,还是把网络内容、模型输出直接交给 shell? | curl \| sh、Invoke-Expression、动态拼接命令 |
| 网络范围 | 访问已知官方域名,还是向未说明地址上传代码、日志或令牌? | 上传仓库内容、发送环境变量、未声明域名 |
如果你还不熟悉技能的目录约定,可以先读 Agent Skills 基础教程,再回到本文逐项核对。重点不是记住某种固定结构,而是找出所有能让代理越过当前项目边界的入口。
⚠️ 注意:README 里的承诺不能替代脚本审查。说明写“只读”,但脚本实际调用删除、下载或上传命令时,应以代码行为为准。
三、安装前按顺序读四类文件
面对几十个文件,不必从第一行漫无目的地读到最后一行。下面的顺序能先暴露高影响行为,再补齐细节。
| 顺序 | 文件类型 | 重点检查 |
|---|---|---|
| 1 | 主说明和触发条件 | 什么请求会激活 Skill,是否可能在普通编码任务中误触发 |
| 2 | 全部脚本 | 进程启动、文件删除、权限修改、下载执行、网络上传 |
| 3 | 依赖与锁文件 | 运行必需依赖、临时工具、postinstall 等自动钩子 |
| 4 | 配置与模板 | 默认输出目录、允许域名、环境变量名、是否要求长期令牌 |
审查时还要留意间接执行。例如 JavaScript 的 child_process、Python 的 subprocess、PowerShell 的 Invoke-Expression,以及把 curl 或 wget 的结果直接通过管道交给 shell。这些能力不一定恶意,但都要求更高等级的证据:命令参数应固定,输入来源应可追踪,写入目标应落在明确目录内。
一个实用做法是先生成文件清单,再按扩展名分组。下面的 PowerShell 命令只读取目录,不修改文件:
# 递归列出候选 Skill 目录下的所有文件
Get-ChildItem .\candidate-skill -Recurse -File |
Select-Object FullName, Length, LastWriteTime |
Sort-Object FullName
清单中若出现二进制文件、压缩包或混淆后的脚本,不能因为文本搜索没有命中就判定安全。你需要追溯它们的来源,或者直接拒绝在真实项目中运行。
四、用只读扫描器筛出高风险动作
人工阅读负责理解上下文,静态扫描负责防止漏看。下面的完整 Python 3.12 程序会遍历 Markdown、PowerShell、Shell、JavaScript、Python 和 JSON 文件,标记管道执行、命令解释器、递归删除以及常见凭证路径。它只读文件,不执行目标 Skill 中的任何命令。
from pathlib import Path
import re
import sys
# 高风险规则:命中只表示需要人工解释,不自动等于恶意
RULES = {
# curl 或 wget 结果直接通过管道交给 shell
"shell_pipe": re.compile(r"(?:curl|wget)\b[^\n|]*\|\s*(?:sh|bash|zsh)\b", re.I),
# PowerShell 动态执行表达式
"powershell_eval": re.compile(r"\b(?:Invoke-Expression|iex)\b", re.I),
# 递归删除文件或目录
"recursive_delete": re.compile(r"\b(?:rm\s+-rf|Remove-Item\b[^\n]*-Recurse)\b", re.I),
# 常见凭证路径或环境变量
"credential_path": re.compile(r"(?:\.ssh|\.aws|\.env|credentials)", re.I),
}
def scan(root: Path):
findings = []
# 只扫描这些文本类扩展名,避免误读二进制
allowed = {".md", ".ps1", ".sh", ".js", ".py", ".json"}
scanned = 0
for path in sorted(root.rglob("*")):
if not path.is_file() or path.suffix.lower() not in allowed:
continue
scanned += 1
text = path.read_text(encoding="utf-8", errors="ignore")
for name, pattern in RULES.items():
for match in pattern.finditer(text):
# 计算命中行号
line = text.count("\n", 0, match.start()) + 1
findings.append((name, path.relative_to(root), line))
return findings, scanned
# 要求传入一个目录参数
if len(sys.argv) != 2:
raise SystemExit("usage: python scan_skill.py <skill-directory>")
root = Path(sys.argv[1]).resolve()
findings, scanned = scan(root)
for name, path, line in findings:
print(f"RISK {name} {path}:{line}")
if findings:
print(f"FAIL: {len(findings)} suspicious occurrence(s)")
raise SystemExit(1)
else:
print(f"PASS: scanned {scanned} file(s), no rule matched")
raise SystemExit(0)
本文使用 Python 3.12 完成实际运行,扫描了两个最小夹具。安全夹具的原始输出是:
PASS: scanned 1 file(s), no rule matched
含 Invoke-Expression 测试文本的夹具返回退出码 1,原始输出是:
RISK powershell_eval SKILL.md:9
FAIL: 1 suspicious occurrence(s)
| 规则名 | 匹配内容 | 风险说明 |
|---|---|---|
shell_pipe |
curl \| sh、wget \| bash |
下载后直接执行,来源不可控 |
powershell_eval |
Invoke-Expression、iex |
动态执行字符串,可能被注入 |
recursive_delete |
rm -rf、Remove-Item -Recurse |
可能删除项目外文件 |
credential_path |
.ssh、.aws、.env、credentials |
可能读取敏感凭证 |
命中规则只表示“需要人工解释”,不自动等于恶意;没有命中也不等于绝对安全。扫描器看不懂动态拼接、编码载荷和二进制程序,所以它是漏检保护网,不是最终裁判。
五、在隔离副本里试跑并记录变化
静态检查通过后,也不要直接把 Skill 指向主仓库。先复制一个不含真实密钥的最小项目,关闭不必要的网络权限,只开放一个临时输出目录。若代理支持逐次审批工具调用,应在第一次运行时保持审批开启。
试跑前记录基线:
# 查看当前工作区文件变化
git status --short
# 只列出环境变量名称,避免把值写入日志
Get-ChildItem Env: | Select-Object Name
第一条帮助你对比文件变化;第二条只列环境变量名称,避免把值写入日志。
试跑完成后再次执行 git status --short,并检查临时目录外是否出现新文件。若 Skill 宣称只生成报告,却修改源码或锁文件,行为与说明不一致,应立即停止。
| 试跑阶段 | 操作 | 记录内容 |
|---|---|---|
| 试跑前 | 复制最小项目,关闭不必要网络 | 基线文件状态、环境变量名 |
| 试跑中 | 保持工具调用审批 | 批准过的命令、网络目标 |
| 试跑后 | 对比 git status 和临时目录 |
变更文件、新文件、退出码 |
| 异常时 | 立即停止并保留日志 | 失败原因、触发命令 |
想进一步理解代理的工具调用、上下文与验收方式,可以结合 AI 编程技能教程 补齐基础。真正可靠的试跑记录至少包含:输入任务、批准过的命令、网络目标、变更文件、退出码和失败原因。不要只保留最后一句“完成”。
💡 小提示:隔离试跑最好使用可丢弃副本或临时工作树。这样即使脚本写错路径,也不会把未提交工作一起带走。
六、根据证据决定安装、收紧或拒绝
审查结果不只有“安全”和“不安全”两档。更实用的决策是把能力与任务相匹配。
| 证据情况 | 决策 | 处理方式 |
|---|---|---|
| 文件透明、命令固定、写入范围明确 | 可以安装 | 仍保留首次运行审批 |
| 功能合理,但权限范围过宽 | 收紧后安装 | 改成项目内相对路径,移除非必需网络和自动安装 |
| 脚本下载后直接执行,来源或校验不明 | 拒绝 | 要求提供固定版本、哈希和可读源码 |
| 会读取凭证或上传仓库内容,但用途未说明 | 拒绝 | 不提供令牌,不在真实项目试跑 |
| 只有二进制文件或混淆代码 | 暂停 | 要求源码、构建说明和可复现产物 |
同一个 Skill 在不同项目中的风险也不同。只读公开示例仓库时,影响可能有限;接触公司源码、生产密钥或签名证书时,任何额外权限都会放大后果。安全审查的核心不是追求“零风险”,而是让权限、证据和业务价值处在同一量级。

总结
第三方 Skill 安全审查要围绕真实执行边界展开:先清点文件,再读脚本和依赖,用只读规则筛查高风险动作,最后在隔离副本里观察文件、命令和网络变化。
你可以带走三条判断:
- 功能说明只能解释目的,脚本和工具调用才能证明行为;
- 静态扫描负责发现线索,隔离试跑负责验证实际影响;
- 权限说不清、来源不可追溯或失败后无法回退时,拒绝安装比事后补救更便宜。
延伸学习
想继续练习 AI 编程代理的安全使用,可以按这个顺序来:
- 先跟着 Lingma AI 开发实战课程 熟悉真实开发任务中的代理协作;
- 再阅读 OpenClaw 安全风险笔记,理解权限和外部服务带来的风险面;
- 最后用 OpenClaw Skills 教程 对照一个完整技能系统的结构。
常见问题
Q:扫描器没有报错,就能直接安装吗?
A:不能。没有报错只说明这些简单规则没有命中。动态拼接命令、二进制程序和运行时网络行为仍需人工阅读与隔离试跑确认。
Q:Skill 要求读取环境变量一定有问题吗?
A:不一定。调用 API 可能确实需要令牌,但它应只读取明确名称,不能枚举并输出全部环境变量,更不能把令牌写入日志或上传到未说明的域名。
Q:发现递归删除命令应该怎么处理?
A:先停止安装,确认目标路径是否固定且受项目根限制。若路径来自模型输出、环境变量或字符串拼接,就不应让它在真实工作区自动执行。
Q:开源仓库里的 Skill 就一定安全吗?
A:不一定。开源提高了可审查性,却不能代替版本固定、依赖审计和运行边界。安装时仍要确认具体提交、文件差异和依赖来源。

免费 AI IDE



