LLM Agent Skills 正在成为 Agent 应用里复用能力的主要方式,但随之而来的凭据(Credentials)泄露风险往往被开发者低估。一篇题为《Credentials Are Leaked by LLM Agent Skills: An Empirical Study》的研究把这个问题摆到台面上:API Key、数据库口令、云厂商访问密钥、OAuth Token,都有可能在 Skill 的输出、日志、错误信息或仓库文件中被意外暴露。下面这篇文章会结合 Agent Skills 的运行机制,先解释凭据为什么容易在这里泄露,再提供一个可本地复现的最小实验,最后给出检测、排查和生产环境落地的防护建议。无论你是 Agent 应用开发者、LLM 框架使用者,还是负责平台安全的技术人员,都可以按这套方法梳理自己项目里的风险面。
需要先说明一点:这篇文章不引用原论文的具体实验数据和结论,只围绕“Credentials Are Leaked by LLM Agent Skills”这个事实方向,搭建一套可自行验证的安全分析框架。所有实验都使用伪凭据,不连接真实服务,也不依赖外部模型厂商。
1. Agent Skills 的凭据风险要从机制说起
1.1 从“工具”到“技能”,安全边界发生了转移
在传统 Function Calling 或者插件模型里,开发者会为模型注册一组函数:
{ "name": "query_order", "description": "查询订单信息", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"} }, "required": ["order_id"] } }函数本身只是描述,真正的业务逻辑由后端服务执行,执行结果是否返回给模型、返回哪些字段,还可以在代码里做二次控制。Agent Skills 则更进一步。一个 Skill 更像是一个可以独立执行的小任务单元,通常由三部分组成:
- 一份描述文件,比如
skill.yaml,说明这个 Skill 是干什么的、需要哪些环境变量、执行什么命令; - 一段可执行脚本或命令,比如 Python、Shell;
- 一个输出结果,执行后的 stdout、stderr 或者返回 JSON 会被组装回 Agent 的上下文。
这种设计让模型可以“看懂”技能并动态决定是否调用,但安全边界也随之转移。普通脚本是开发者写死调用链路的,输出数据不会自动进入模型上下文;而 Agent Skill 的调用决策交给模型,执行产生的原始输出又常常原样拼进上下文。两边叠加,凭据暴露的窗口比静态脚本大很多。
下表整理了几个关键差异。
| 对比维度 | 传统 Function Calling | Agent Skills |
|---|---|---|
| 能力载体 | 函数、后端接口 | 脚本、命令、描述文件 |
| 谁决定调用 | 开发者编排或模型按 schema 调用 | 模型根据 description 自主选择 |
| 输出流向 | 可以结构化过滤 | 往往是文本回传,容易被透传 |
| 凭据管理位置 | 集中在后端服务 | 分散在 Skill 目录、配置和环境变量里 |
| 风险特征 | 边界清晰,容易收敛 | 边界分散,需要额外脱敏处理 |
1.2 一次 Skill 调用的完整生命周期与凭据接触点
要理解凭什么据会泄露,先看一个 Agent Skill 从定义到执行的完整过程:
- 开发者编写 Skill 目录,包含描述文件和执行脚本;
- 将 Skill 注册到 Agent 编排框架;
- Agent 运行时加载 Skill 元数据,把描述和参数说明提供给模型;
- 模型根据用户请求选择某个 Skill;
- 框架执行 Skill 的 command,捕获 stdout 和 stderr;
- 执行结果被写入 Agent 对话上下文;
- 日志系统记录本次调用的元数据、入参和输出。
凭据在这个流程里有几个潜在接触点:
- 第 1 步:如果凭据写在脚本、
.env、skill.yaml里,源码和配置就是泄露源; - 第 5 步:脚本在调试日志或 stdout 里打印环境变量,凭据会进入执行输出;
- 第 6 步:输出中包含的连接串、Token 会进入模型上下文,控制权离开应用层;
- 第 7 步:中心化日志保存了完整 stdout,变成第二个存储侧泄露点。
这里最容易误解的是“只要凭据放在环境变量里就安全”。实际上,环境变量只是不把秘密写死在源码里,但如果程序在日志中打印os.environ、debug 时输出DB_PASSWORD,或者异常信息里包含完整配置对象,凭据依然会从输出通道泄露出去。
1.3 和 n8n、Flowise 等编排平台的 credentials 差异
在 n8n、Flowise 这类工作流平台中,Credentials 通常有独立配置页,用户把 API Key、Token 存到平台的凭据仓库,节点只引用变量名。这种模式相对集中,风险主要集中在平台本身的鉴权和存储。
Agent Skills 不同。当前很多实现是“落盘”的:一个 Skill 就是一个目录,里面除了描述文件,还有脚本、配置,甚至.env。如果采用同样松散的凭据管理方式,每个 Skill 目录都可能成为独立泄露源。这也是实证研究值得关注的原因:当 Skill 数量多起来,凭据不是集中在一个地方,而是散落在几十上百个目录里,检测和治理难度会显著上升。
2. 凭据泄露的四种典型链路
2.1 写入侧:配置、源码和 .env 进入版本库
最常见的一类泄露发生在代码被写下来的一瞬间。Skill 的目录天然是“自包含”的,开发者为了方便,会在脚本里硬编码密钥,或者把DB_PASSWORD写进skill.yaml,再或者在 Skill 目录下放一个.env。
这类问题在普通项目里也有,但 Skill 让问题更容易出现,因为 Skill 的复制传播成本很低:一个写好了数据库凭据的 Skill 可能被团队成员直接复制到另一个项目里,而.gitignore未必覆盖每个 Skill 目录。
还有一种情况是环境变量文件没有被忽略,或者被 CI 过程作为产物上传。此时凭据不仅进入源码仓库,还可能进入构建日志、镜像层、制品仓库。后续清理成本非常高。
2.2 执行侧:debug 日志、stdout 和进程信息
Agent 框架执行 Skill 时,通常会用子进程方式运行命令并捕获输出。看一个常见实现:
import subprocess def run_skill(command: str): output = subprocess.check_output( command, shell=True, text=True, stderr=subprocess.STDOUT ) return output这个代码本身没有把凭据写进日志,但只要 Skill 脚本中写了类似:
logging.debug("connect db user=%s password=%s", user, password)那么password就会通过 stderr 被 subprocess 捕获。再由框架把这个输出组织成结果交给模型,甚至写入日志。
shell=True还会带来另一条泄露路径:当命令字符串包含动态拼接的参数时,整个命令行可能出现在操作系统的进程列表或者 shell 历史里。如果拼接内容里有 Token,凭据就等于落到了进程监控工具和管理员日志里。
2.3 模型侧:输出作为模型上下文,边界不可控
Agent Skill 的价值在于模型能理解执行结果并决定下一步动作。因此框架往往会尽量把 Skill 的 stdout 完整传给模型,而不是只传一个状态码。
问题就在这里。如果脚本返回了这样的结果:
{ "status": "success", "connection": { "host": "127.0.0.1", "user": "demo_user", "password": "FakePass123!" } }这段文本会进入模型上下文。一旦进入模型服务方或者中转链路,应用所在方就失去了对这部分数据的完全控制。即使模型不会主动输出这个字段,数据也已经越过应用边界,进入了第三方可观测范围。这是 Agent Skills 特有的风险,因为普通函数调用通常可以设计“只返回必需字段”,而 Skill 输出往往是原始文本。
这里要强调一点:不要把希望寄托在“模型不会乱说”上。安全边界应该设计在输出形成之前,而不是依赖模型对数据的自动隐藏。
2.4 异常侧:堆栈和回退信息包含连接串
很多开发者在异常处理里习惯“把完整错误信息返回给模型,让模型继续尝试”。例如:
except Exception as exc: return f"执行失败,完整错误:{str(exc)}"如果str(exc)里包含数据库连接字符串、配置对象、环境变量字典,异常消息就会带走凭据。
例如配置对象转字符串后可能是这样:
Config(host=127.0.0.1, user=demo_user, password=FakePass123!, database=demo_db)这类内容进入模型上下文后,日志和追踪系统也会同步记录。异常回退是一个非常隐蔽的通道,因为它不是业务输出,只在任务失败时出现,但恰恰最容易携带调试信息。
2.5 泄露链路横向对比
| 泄露链路 | 发生阶段 | 典型凭据 | 主要通道 |
|---|---|---|---|
| 源码写入 | Skill 开发时 | 硬编码密钥、.env | 版本库、镜像、制品 |
| 执行输出 | 脚本运行时 | 密码、Token、连接串 | stdout、stderr、进程列表 |
| 模型上下文 | 结果回传 | 脱敏缺失的返回体 | LLM 请求内容 |
| 异常回退 | 任务失败时 | 配置对象、异常堆栈 | 错误消息、日志 |
3. 复现研究思路:搭一个最小可运行的凭据泄露实验
原论文的研究方式是实证分析,那我们也用实验思路来建立直觉。下面这个实验不连真实数据库、不调用真实模型,只模拟“Agent Runner 执行 Skill 并捕获输出”这个过程。所有密码、Token 都是伪凭据,即使泄露也不影响任何真实服务。
3.1 实验目标、安全边界和环境准备
实验目标有三个:
- 观察坏写的 Skill 如何把凭据带到 stdout;
- 观察 debug 日志如何把凭据带进 stderr;
- 用一个静态扫描脚本,找出这些疑似凭据点。
环境要求很低:
| 项目 | 要求 | 说明 |
|---|---|---|
| Python | 3.9 及以上 | 仅使用标准库 |
| 操作系统 | Linux / macOS / Windows | 示例命令以 Linux 风格展示 |
| 外部依赖 | 无 | 不需要安装第三方包 |
| 真实凭据 | 不使用 | 全部使用占位值 |
建议准备一个独立目录agent-skills-security-lab,实验结束可以直接删除。不要把这个实验放在真实项目的上层目录里运行,避免扫描出真实环境的敏感信息后误处理。
3.2 准备三个 Skill 样本
目录结构如下:
agent-skills-security-lab/ ├── detector.py ├── runner.py ├── skills/ │ ├── .env.example │ ├── bad_hardcode/ │ │ ├── skill.yaml │ │ └── run.py │ ├── debug_output/ │ │ ├── skill.yaml │ │ └── run.py │ └── safe_skill/ │ ├── skill.yaml │ └── run.py先看坏示例一:把密码放进返回结果里。
# skills/bad_hardcode/run.py import json # 错误示范:为了说明问题,把凭据以调试字段带出 DB_PASSWORD = "FakePass123!" DB_USER = "demo_user" host = "127.0.0.1" result = { "status": "success", "connection": { "host": host, "user": DB_USER, "password": DB_PASSWORD } } print(json.dumps(result, ensure_ascii=False))它的技能描述文件:
# skills/bad_hardcode/skill.yaml name: bad_hardcode description: 查询演示数据库订单数量 type: command command: python run.py env: - DB_HOST - DB_USER - DB_PASSWORD坏示例二:debug 日志输出环境变量。
# skills/debug_output/run.py import os import logging logging.basicConfig(level=logging.DEBUG) DB_USER = os.getenv("DB_USER", "demo_user") DB_PASSWORD = os.getenv("DB_PASSWORD", "fallback_secret") # 错误示范:debug 日志直接记录完整环境变量 logging.debug("开始连接数据库 user=%s password=%s", DB_USER, DB_PASSWORD) print("查询完成")安全示例:不打印凭据,只输出业务状态和会话信息。
# skills/safe_skill/run.py import os import logging logging.basicConfig(level=logging.INFO) DB_USER = os.getenv("DB_USER", "") # 演示从外部临时凭据源获取,不在脚本里硬编码 token = os.getenv("DB_TOKEN", "") # 记录时只保留脱敏信息,不打印完整 token safe_token = token[:4] + "****" if token else "N/A" logging.info("connect db=demo_orders user=%s token=%s", DB_USER, safe_token) print("query_ok")再放一个常见的.env.example,演示“配置文件即使只是模板,也可能被扫描器识别”。
# skills/.env.example DB_HOST=127.0.0.1 DB_USER=demo_user DB_PASSWORD=change_me这里的关键点是:safe_skill仍然打印了token=abcd****,但只保留前 4 位,并且整体是脱敏格式。实际生产里还可以更保守,比如日志里只打印session_id,完全不出现 token 前缀。
3.3 用 Runner 模拟 Agent 调度过程
下面写一个简化版 Runner,遍历skills目录下的每个 Skill,执行skill.yaml里声明的命令,捕获 stdout 和 stderr。它模拟的是主流 Agent 框架“执行 Skill -> 收集输出 -> 返回给模型”的核心链路。
# runner.py import os import subprocess from pathlib import Path BASE_DIR = Path(__file__).parent SKILLS_DIR = BASE_DIR / "skills" def run_skill(skill_dir: Path) -> str: # 演示场景固定执行 python run.py # 真实项目应该解析 skill.yaml 中的 command 字段 command = "python run.py" try: output = subprocess.check_output( command, cwd=skill_dir, shell=True, text=True, stderr=subprocess.STDOUT, ) return f"[stdout]\n{output}" except subprocess.CalledProcessError as exc: return f"[failed]\n{exc.output}" def main(): for skill_dir in sorted(SKILLS_DIR.iterdir()): if not skill_dir.is_dir(): continue if not (skill_dir / "skill.yaml").exists(): continue print(f"==== Skill: {skill_dir.name} ====") print(run_skill(skill_dir)) print() if __name__ == "__main__": main()这里用shell=True是为了模拟很多真实项目里的写法和风险。生产环境不建议这样做,尤其是当 command 字符串包含外部输入或环境变量时,应当改用参数列表方式调用,避免命令注入和进程列表泄露。
3.4 运行实验,查看泄露现象
在项目根目录执行:
cd agent-skills-security-lab python runner.py预期输出如下:
==== Skill: bad_hardcode ==== [stdout] {"status": "success", "connection": {"host": "127.0.0.1", "user": "demo_user", "password": "FakePass123!"}} ==== Skill: debug_output ==== [stdout] DEBUG:root:开始连接数据库 user=demo_user password=fallback_secret 查询完成 ==== Skill: safe_skill ==== [stdout] INFO:root:connect db=demo_orders user=demo_user token=N/A query_ok可以看到前两个 Skill 都出现了明显的凭据泄露:
bad_hardcode把password放进了返回 JSON;debug_output把password打印到了日志输出;safe_skill只暴露了脱敏后的 token 前缀,风险可控。
如果这个实验接入真实模型,bad_hardcode和debug_output的整段输出都会进入模型上下文。即使模型本身不记录这些内容,凭据也已经通过了应用层边界。
3.5 用 detector.py 做静态扫描
静态扫描不能替代人工审计,但可以快速发现已知模式。下面写一个启发式扫描器,识别password、api_key、secret、token、云访问密钥和私钥片段。
# detector.py import re import sys from pathlib import Path SECRET_PATTERNS = [ (re.compile(r"(?i)(password|passwd|pwd)\s*[:=]\s*\S+"), "password"), (re.compile(r"(?i)(api[_-]?key|secret|token)\s*[:=]\s*\S+"), "api key/secret/token"), (re.compile(r"(?i)(AKIA[0-9A-Z]{16})"), "cloud access key sample"), (re.compile(r"(?i)(BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY)"), "private key"), ] EXCLUDE = {".git", "__pycache__", ".venv", "node_modules"} def scan_file(path: Path): findings = [] try: text = path.read_text(encoding="utf-8", errors="ignore") except Exception: return findings for lineno, line in enumerate(text.splitlines(), 1): for pattern, name in SECRET_PATTERNS: if pattern.search(line): findings.append((lineno, name, line.strip())) return findings def main(root: Path): total = 0 for path in root.rglob("*"): if path.is_dir(): continue if any(part in EXCLUDE for part in path.parts): continue if path.suffix == ".pyc": continue findings = scan_file(path) for lineno, name, line in findings: total += 1 print(f"{path}:{lineno} [{name}]") print(f" -> {line[:160]}") print(f"\n扫描完成,共发现 {total} 处疑似凭据点。") if __name__ == "__main__": main(Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).parent)执行扫描:
python detector.py .预期输出类似:
skills/.env.example:3 [password] -> DB_PASSWORD=change_me skills/bad_hardcode/run.py:9 [password] -> result = {"status": "success", "connection": {"host": host, "user": DB_USER, "password": DB_PASSWORD}} skills/debug_output/run.py:10 [password] -> logging.debug("开始连接数据库 user=%s password=%s", DB_USER, DB_PASSWORD) 扫描完成,共发现 3 处疑似凭据点。从这个结果能看出两个重要事实:
- 静态扫描可以发现源码和配置里的凭据点,但未必能发现运行期动态拼出的凭据;
- 即使扫描到
password,也需要人工判断是真实泄露还是字符串字面量误报。
所以扫描是第一步,运行期检测和日志审查才是完整闭环。
注意:实验中的所有伪凭据只用于本地演示。不要把
.env.example、FakePass123!、change_me直接当成安全示例复制到生产项目。
4. 输出侧排查:在凭据进入模型上下文之前拦下来
4.1 先判断泄露发生在哪一个接触点
拿到一份泄露报告时,不要急着改日志,先判断泄露点:
| 泄露现象 | 大概率发生位置 | 排查方向 |
|---|---|---|
| Git 历史里有密码 | 写入侧 | 查.gitignore、历史提交、镜像层 |
| 运行时 stdout 有 Token | 执行侧 | 查脚本 print、json dumps、调试输出 |
| 日志系统出现凭据 | 日志侧 | 查 logging 配置、日志采集管道 |
| 模型输出里出现凭据 | 上下文侧 | 查传给模型的完整结果、异常消息 |
| 进程列表出现 Token | 命令执行侧 | 查shell=True、字符串拼接命令 |
排查顺序建议是:先看输出,再看日志,再查源码写入,最后检查模型上下文。输出不泄露,日志通常也不存在;源码和配置是源头,必须同时收敛。
4.2 输出侧拦截:设计统一的脱敏层
Agent 框架里比较有效的做法是,在“Skill 输出”进入模型上下文之前加一层统一脱敏函数。它的职责是扫描所有要回传给模型的文本,把高风险字段替换成掩码。
import re # 统一脱敏规则:匹配常见凭据字段 REDACT_PATTERNS = [ re.compile(r"(?i)(password|passwd|pwd)\s*[:=]\s*\S+"), re.compile(r"(?i)(token|api[_-]?key|secret)\s*[:=]\s*\S+"), re.compile(r"(?i)(Bearer\s+)[A-Za-z0-9._~+/-]+=*"), ] def redact_output(text: str) -> str: result = text for pattern in REDACT_PATTERNS: result = pattern.sub(r"\1***", result) return result使用时机很关键:脱敏层不能放在 Skill 脚本内部,因为每个 Skills 写法都不一样;应该放在框架层,作为所有 Skill 输出的统一出口。这样即使某个 Skill 写得不规范,框架也能兜底。
脱敏层也会带来副作用:内容被掩码后,模型可能无法基于真实字段继续完成任务。因此更合理的做法是先在 Skill 规范里约定“任何凭据字段不得出现在返回值中”,脱敏层只是最后一道防线。
4.3 日志与追踪链路排查
日志系统是凭据的第二个“记忆仓库”。即使 stdout 没有泄露,只要日志采集器把 stderr 和完整堆栈写入 Elasticsearch、Loki 或云日志服务,凭据就会长期保存。
排查日志链路时重点看三处:
logging.basicConfig(level=logging.DEBUG):项目里是否存在过宽的日志级别;- 是否把
os.environ、配置对象、异常对象直接str()后写入日志; - 日志采集器是否自动记录环境变量或 HTTP Header。
如果使用的是开源日志协议,比如 OpenTelemetry,要关注 Span Attribute 里是否填充了敏感字段。很多默认采集不会自动脱敏,需要显式配置密钥过滤规则。
4.4 常见坑与处理建议
| 常见坑 | 为什么危险 | 处理建议 |
|---|---|---|
.env文件被提交到 Git | 凭据进入版本历史和分发渠道 | 删除历史、轮换凭据、完善.gitignore |
debug 日志打印os.environ | 环境变量字典包含全部密钥 | 只打印变量名,不打印值 |
| Skill 返回 JSON 带调试字段 | 输出会直接进入模型上下文 | 返回前白名单过滤字段 |
异常处理返回str(exc) | 连接串、配置对象可能被包含 | 返回错误码和摘要,屏蔽敏感字段 |
shell=True拼接命令行 | 命令字符串进进程列表和 shell 历史 | 使用参数列表方式调用 |
| 集中式日志未脱敏 | 凭据被持久化保存 | 日志采集前做掩码处理 |
5. 生产环境防护落地
5.1 凭据注入方式:从“跟随 Skill”改为“运行期注入”
最核心的迁移目标,是让 Skill 目录里不出现任何凭据。Skill 打包、分发、复制时,目录里只有代码和描述文件,没有.env,没有硬编码密钥。
运行期注入的方案可以按项目复杂度选择:
| 方案 | 适用场景 | 优点 | 注意点 |
|---|---|---|---|
| 进程环境变量 | 小型项目 | 简单直接 | 不许打印,不许写日志 |
| 密钥管理服务 | 中大型项目 | 集中存储、审计 | 需要网络权限和 SDK |
| OIDC 短期凭据 | 云原生容器或 CI | 无长期密钥,自动轮换 | 需要平台支持和角色配置 |
如果暂时无法迁移到密钥管理服务,也应该立即执行两条规则:Skill 目录不允许存在.env文件;所有脚本只允许从外部注入的环境变量读取密钥,不允许在代码里写默认密钥。
5.2 最小权限和短期凭据
给每个 Skill 分配独立身份,而不是让所有 Skill 共用一个高权限账号。独立身份的意义在于:一个 Skill 泄露凭据,只影响它对应的函数、表和 API 范围,不会波及其他能力。
配合短期凭据使用效果更好。例如数据库可以使用临时密码,云资源可以用 OIDC 换取短期 Token。短期凭据过期后即使进入日志,价值也会大大降低。
5.3 生命周期治理:写前扫描、运行监控、日志脱敏
凭据治理应该是持续过程,而不是上线前的一次性检查:
- 写代码阶段:IDE 插件或 pre-commit 钩子扫描新增代码;
- 提交阶段:CI 里跑 gitleaks、trufflehog 等工具;
- 运行阶段:框架层统一脱敏输出;
- 日志阶段:日志采集后执行敏感字段过滤和告警;
- 响应阶段:发现泄露后立即轮换凭据,并调查泄露范围。
考虑到工具版本和项目环境的差异,这里不展开某个工具的安装步骤,落地前要以当前项目使用的版本和文档为准。核心思路是“每一层都设一道闸门”,不能只靠开发者自觉。
5.4 学习环境、测试环境、生产环境差异
| 维度 | 学习环境 | 测试环境 | 生产环境 |
|---|---|---|---|
| 凭据来源 | 可写死或使用占位值 | 独立测试库,最小权限 | 密钥管理服务或短期 Token |
| 日志级别 | DEBUG 可开 | INFO/WARN,脱敏后查 | 强制脱敏,禁止打印凭据 |
| 模型调用 | 可控沙箱,使用伪数据 | 严格限制返回内容 | 统一脱敏层兜底 |
| 泄露响应 | 删除实验目录即可 | 轮换测试凭据 | 事件响应,撤回历史,通知相关方 |
注意:不要用生产凭据在测试环境里跑实验。测试环境一旦接入真实密钥,泄露影响并不比生产环境小多少。
6. 团队可复用清单与后续方向
6.1 上线前检查清单
每次发布一个 Agent Skill 前,至少检查以下项目:
- Skill 目录里是否存在
.env、密钥文件、证书、.pem; skill.yaml中是否有password、token、api_key字段;- 运行脚本是否把环境变量或配置对象输出到日志;
- 异常处理是否会把完整堆栈返回给模型;
- 框架层是否有输出脱敏函数;
- 日志采集系统是否配置了敏感字段过滤;
- 该 Skill 使用的凭据是否对应最小权限账号;
- 凭据是否设置了过期时间;
- 是否能在中心化日志系统里搜索到该 Skill 名称,并确认没有敏感数据;
- 是否把实验目录排除在扫描范围之外。
6.2 泄露事件的响应路径
如果已经确认凭据泄露,按以下顺序处理:
- 立即吊销或轮换泄露的凭据;
- 确认泄露范围:源码仓库、日志系统、镜像、模型上下文、第三方服务;
- 检查泄露凭据的权限范围,判断是否还需要撤销相关账号权限;
- 定位泄露源头,修复写入或输出代码;
- 增加自动检测规则,避免同类问题再次出现;
- 保留审计记录,确认是否有异常访问。
不要等到“定位到根因再轮换密钥”。凭据一旦进入模型上下文或日志系统,就无法确认谁看到过,第一时间轮换是止损的前提。
6.3 如何持续验证防护效果
持续验证不能只靠代码 review,建议周期性执行三件事:
- 随机抽取部分 Skill,运行实验脚本并抓取 stdout、stderr、返回给模型的文本,检查是否出现占位密文;
- 在测试环境用一组“伪泄露样例”验证检测规则是否有效;
- 定期扫描 Git 历史,确认历史提交中不会重新出现凭据。
如果团队引入了观测系统,还可以关注三类指标:隐藏凭据检测命中数、日志脱敏覆盖率、密钥轮换频率。检测命中数快速下降不一定代表绝对安全,但持续为零且覆盖范围明确,至少说明基础面被控制住了。
Agent Skills 给自动化和 LLM 应用带来了很灵活的封装方式,但这份自由也意味着输出不能被当作可信数据直接消费。凭据泄露并不是单个开发者编码习惯差造成的,而是 Skill 这种封装单元天然把命令、配置和输出绑在了一起。设计阶段就让 Skill 不持有凭据,运行时从外部获取最小权限的短期身份,输出前用统一脱敏层拦截,存储侧坚持扫描仓库、日志和 CI,Agent 的自动化程度越高,凭据反而应该越安全。