SKILL.md里写不下授权:AI编程助手的认证该放哪一层?
最近帮团队调一个 AI 编程助手的技能配置卡在最不起眼的一步上怎么在 SKILL.md 里把“授权”这件事说清楚。同事把内部 API 的 Token 直接贴进了技能文件觉得“这样 AI 就能自己调接口了”结果评审直接被安全组打回。问题倒不是格式写得不对而是这个文件本身就不是用来装授权的。SKILL.md 是给大模型看的“技能说明书”不是给程序跑的“授权配置”。这个区别想不明白后面基本就是在帮 AI 的权限漏洞打补丁。今天就把这个坑摊开聊。我会从 SKILL.md 的文件本质、授权场景的硬边界、正确分层方式到一份可以直接抄作业的“带授权调用”技能模板完整过一遍。适合正在折腾 AI 编程助手技能文件的朋友以及想搞清楚“认证授权到底该放哪层”的开发者。1. 先搞清楚 SKILL.md 是什么为什么老有人想往里塞授权1.1 它是给大模型的“岗位说明书”不是可执行代码SKILL.md 是近两年 AI 编程助手生态里流行起来的一种技能描述文件通常放在项目的.claude/skills/、.cursor/skills/这类目录下。它用 Markdown 写内容一般包含技能名称、触发条件、使用步骤、示例代码、注意事项。实际工作方式是把这些文字当作上下文注入给大模型让模型在执行任务时“知道”有这么一个技能存在以及按什么步骤去用它。打个比方这就像你给新员工发了一本 SOP 手册上面写着“管好门禁、登记来访、注意消防通道”。新员工读了之后知道该做什么但手册本身不包含门禁系统的数据库密码也不包含发卡机怎么接线。SKILL.md 就是这本手册它影响的是大模型的判断和动作而不是直接替换掉运行环境里的权限机制。很多人栽跟头就是把它当成了“配置文件”甚至“可执行脚本”往里塞环境变量、API Key、OAuth 回调逻辑指望 AI 读了 SKILL.md 就能自己完成鉴权。这从一开始就跑偏了。1.2 想往里塞授权的三个典型误解我复盘了身边好几个项目案例包含我自己第一次写技能文件时的操作发现大家的误区高度一致第一个误解觉得 SKILL.md 能代替环境变量。写过几年代码的人都清楚密钥的正确位置是.env文件、密钥管理服务、或者 CI 里的 secret 配置。SKILL.md 是明文文本会被同步进仓库、被 AI 服务读取、被日志记录把密钥写进去等于把工牌落在广场上。第二个误解认为授权流程能在技能描述里“讲清楚”。OAuth 2.0 的授权码流程需要后端接收回调、保存 state、交换 token、处理 refresh token 轮换这是有状态的运行时逻辑。SKILL.md 只是一段静态文字它描述得再详细AI 也没有能力替你去跑一个 HTTP 回调服务器。第三个误解觉得“写在文件里安全”。有个真实案例某团队在技能文件里写死了数据库密码几个月后员工把项目传到公开仓库密码直接被爬虫抓走云厂商的密钥扫描在几分钟内就自动标记了风险。文件里的文字只要进入版本库就有泄露路径。如果你现在正打算把授权信息写进 SKILL.md先停下来。下面这些硬边界会告诉你为什么写不下。2. 授权为什么在 SKILL.md 里“写不下”四个层面的硬边界2.1 静态文档承载不了动态流程OAuth 是对话不是说明授权和认证最麻烦的地方在于它是一个“动态过程”。拿 OAuth 2.0 授权码模式来说完整流程是这样的用户点击授权、平台重定向到回调地址、后端用授权码换 access token、定期用 refresh token 换新 token。每一步都依赖运行时状态比如 state 参数的校验、token 的存储、过期时间的判断。SKILL.md 里能写什么呢它只能写“请调用某个脚本完成 OAuth 授权”或者“确认环境变量 ACCESS_TOKEN 是否有效”。文件本身没有能力发起重定向、没有地方保存 state、更没法在 token 过期时自动完成刷新——那是程序的工作不是说明书的。我见过有人试图在 SKILL.md 里用大段文字把 OAuth 流程写清楚让 AI“理解”之后用 curl 手动模拟。这个思路在沙盒环境里偶尔能跑通一次但一旦遇到回调域名不匹配、授权码只能使用一次、refresh token 被轮换这些实战情况马上就会碎一地。授权不是一个“读一遍就会”的知识它是一个需要运行时完成的事务。这里还要单独提一下网页授权回调域名。很多第三方平台要求你在应用配置里填写 redirect_uri而且这个回调地址必须精确匹配。本地开发的时候回调地址是http://localhost:8080/callback部署之后变成https://api.example.com/callback每次切换环境都涉及到重新配置。SKILL.md 里如果写死了某个地址换一个环境就失效如果写成“按实际情况调整”AI 又没有能力替你改平台侧的配置。这就是典型的“写不下”。2.2 敏感信息放进上下文等于把密钥交给对话记录这是最容易被忽略的安全红线。AI 编程助手的核心工作方式是对话你的每一次请求、每一段上下文都可能被写入会话历史、被团队共享、被 API 服务商的日志捕获。你正在用的编程助手再把上下文发送给大模型服务端整个链路里任何一环存在日志策略密钥就等于曝光了。更现实的问题是大模型会“复述”上下文。如果你把 API Key 写进 SKILL.md某一次对话里模型为了“帮助你调试”很可能顺手就把密钥打印出来。我真实遇到过模型检查环境变量时把未脱敏的 token 直接输出到聊天窗口在场好几个同事截图记录后续不得不紧急轮换所有密钥。所以凡是需要保密的信息都不应该出现在给大模型看的任何文件里——包括 SKILL.md。这句话值得刻在技能目录的 README 第一行。2.3 SKILL.md 的触发模型里没有“资格”概念从权限设计的角度看SKILL.md 是一个纯描述性的文件。它告诉 AI“这个技能是做什么的、怎么用”但没有任何机制表达“谁有资格用这个技能”。举个场景一个技能可以调用财务系统的接口普通开发者看到了这个技能文件理论上就可以让 AI 去执行。如果没有外部权限拦截这等于给每个能接触到仓库的人发了一张最高权限门禁卡。正确的做法是把“资格校验”放在外部层。例如在调用财务接口的外部脚本里强制校验用户角色SKILL.md 只描述“需要先运行check_permission.js通过后才继续”而真正的角色判断在脚本里用运行时上下文完成。这叫作“策略与机制分离”文件负责表达策略程序负责实施机制。2.4 上下文窗口与维护成本授权细节会污染技能质量还有一个很多人没意识到的问题SKILL.md 会被整个注入到上下文里占用的 token 直接影响模型的表现。如果一份技能文件塞满了授权细节——各种 endpoint、密钥名、重试规则、刷新逻辑——它可能占掉一两千 token 的上下文空间而这些内容对 AI 判断“这个任务该怎么做”完全无益甚至会导致模型把注意力放错地方。维护成本更痛。授权信息会变平台接口更新了、密钥轮换了、不同环境有不同配置。如果你把这类信息写进 SKILL.md就意味着每次变更都要改文档而且同一个技能文件没法同时适配 dev 和 prod 两套凭证。我在一个项目里试过把 API Key 直接写进 SKILL.md后来密钥轮换的时候我要同步改三四个技能的描述文件改完还得担心 AI 记住的是不是旧值——纯纯的自我折磨。3. 授权场景里 SKILL.md 的正确打开方式策略留在文件里机制移到运行时3.1 一句话原则文件只回答“要不要、找谁拿、失败怎么办”把授权从 SKILL.md 里拔出来之后并不是说什么都不写而是要重新划分职责。SKILL.md 适合回答三个问题第一当前任务在什么情况下需要授权第二授权凭证从哪里获取通常是环境变量名或外部命令第三授权失败之后应该引导用户执行什么操作比如运行某个授权脚本、打开某个链接。举个例子一份技能文件里你可以写“调用发布接口前先检查 POST_TOKEN 环境变量是否存在如果不存在运行node scripts/auth.js --login引导授权”。这段文字的作用是让 AI 按照既定流程行动而真正处理 token 的获取、刷新、存储全部发生在auth.js这个外部脚本里。反之不要写“token 是 abc123、回调地址是 https://example.com/callback、过期时间 7200 秒”。这些是运行时数据不是技能知识。3.2 SKILL.md 里该写的三件事结合我自己的实践一份处理授权的 SKILL.md 至少应该包含以下三块内容前置条件检查。明确告诉 AI 在执行任何需要凭证的操作之前先检查环境变量或授权文件是否存在。比如“操作前先确认GITHUB_TOKEN已设置未设置则提醒用户执行登录命令”。授权的获取入口。不要试图在文件里描述整个授权流程只写“通过scripts/auth.py完成授权授权成功后凭证保存在~/.config/xx/token.json”。AI 看到这样的描述会自己决定去调用脚本。失败处理路径。写明当接口返回 401/403 时的处理方式例如“提示 token 已过期执行scripts/auth.py --refresh重新获取”。这一步极其重要不然 AI 遇到鉴权错误时会开始瞎猜甚至尝试绕过校验。模板大致长这样--- name: publish_blog description: 发布一篇博客文章到内容平台需要平台授权。 --- ## 前置条件 - 检查环境变量 PUBLISH_API_TOKEN。 - 如果变量不存在运行 python scripts/auth.py --login 引导用户完成授权。 ## 发布步骤 1. 调用发布接口 POST /v1/publish请求头携带 Authorization: Bearer $PUBLISH_API_TOKEN。 2. 如果返回 401提示用户运行 python scripts/auth.py --refresh 刷新凭证然后重试。 3. 发布成功后返回文章链接。注意上面这份文档里没有任何真实密钥、没有回调地址、没有 token 的过期时间。它只告诉 AI“检查什么、卡住了怎么处理”剩下的机制全部交给外部实现。3.3 运行时层做什么环境变量、外部脚本、MCP 配置真正处理授权的地方在 SKILL.md 之外。凭证放环境变量或系统密钥库这是第一层OAuth 这类交互式流程放外部脚本这是第二层如果工具生态支持 MCPModel Context Protocol或者自定义 tools直接把鉴权逻辑封装成工具服务这是最干净的第三层。以 MCP 为例你可以写一个专门负责“获取授权 token”的 serverAI 只负责调用get_token这个工具。至于 token 怎么签发、怎么刷新、回调地址配置在哪里AI 完全不感知。这比在 SKILL.md 里写一百行授权说明都要可靠。需要说明的是不是所有场景都值得搭 MCP。个人项目或轻量脚本环境变量加外部授权脚本完全够用团队级项目、多服务调用建议走 MCP 或独立授权服务。选型标准很简单看凭证的数量和变更频率。凭证越少、越稳定越没必要上重机制。4. 实操案例做一个带授权调用的“发布文章”技能4.1 需求与目录结构下面我直接给一个可落地的完整案例场景是让 AI 助手调用内容平台的 API 自动发布博客。平台用的是 OAuth 2.0 授权码模式发布接口要求请求头携带Authorization: Bearer的 access tokentoken 有效期两小时支持 refresh token 刷新。项目结构如下project/ ├── .env # 存放 CLIENT_ID、CLIENT_SECRET、REFRESH_TOKEN ├── .env.example # 脱敏模板提交到仓库 ├── skills/ │ └── publish-blog/ │ └── SKILL.md # 技能描述文件 ├── scripts/ │ ├── auth.py # 授权与刷新逻辑 │ └── publish.py # 调用发布接口的封装.env只放在本地环境.env.example里写清楚需要哪些变量名但不填任何真实值。SKILL.md 放在技能目录里用来给 AI 读但它只引用环境变量名和脚本入口不包含任何密钥。4.2 SKILL.md 文件模板下面是这份技能的 SKILL.md 完整内容我实际就在类似项目里这么用可以直接复制改--- name: publish_blog description: 将本地 Markdown 文件发布到内容平台。需要有效的平台授权如果未授权会引导用户完成流程。 --- ## 使用前提 - 发布前先运行 python scripts/auth.py --status 检查授权状态。 - 如果状态不是 “active”运行 python scripts/auth.py --login 完成授权。 ## 授权说明 - 授权凭证保存在本项目本地文件不要读取或打印凭证内容。 - 如果访问令牌过期publish.py 会自动触发刷新逻辑无需用户手动干预。 - 如果刷新失败返回错误信息授权已失效请运行 python scripts/auth.py --login。 ## 发布流程 1. 执行 python scripts/publish.py local_md_path。 2. 等待脚本输出结果如果输出 {ok: true} 则发布成功。 3. 如果输出 {ok: false, error: unauthorized}提示用户重新授权后重试。这份文档的妙处在于AI 通过它知道“何时检查授权、授权在哪里做、失败怎么反馈”但整个 OAuth 交互对 AI 是黑盒。它不需要理解回调域名是什么也不需要知道 token 怎么旋转这些都在auth.py里完成。4.3 外部授权脚本的核心逻辑auth.py是整个流程里真正干活的部分。核心逻辑分四块第一块检查状态。读取本地 token 文件判断 access token 是否存在、是否过期过期则尝试用 refresh token 刷新。第二块刷新令牌。调用平台提供的/v1/oauth/token接口携带 refresh token 换取新的 access token然后写回本地 token 文件。这里要注意某些平台在刷新响应里还会返回新的 refresh token需要一并更新存储这就是 refresh token 轮换。第三块处理回调。干净的做法是启动一个本地临时 HTTP 服务监听http://localhost:8899/callback然后在.env里把REDIRECT_URI配成这个地址。平台侧的回调域名也必须精确配置成http://localhost:8899/callback两端不一致授权码就换不到 token。第四块安全存储。token 文件写在项目外的用户目录比如~/.skill_auth/publish_blog_token.json文件的权限设为只读当前用户。这样即使技能目录被反复同步到仓库天然风险也降到最低。第四块我单独展开一下不要在 token 文件里明文存 CLIENT_SECRET。CLIENT_SECRET 属于静态敏感信息放在.env且不进版本库token 文件里只存 access token 和 refresh token。如果平台没有返回新的 refresh token那就沿用旧的直到 refresh 失败才要求用户重新走登陆流程。4.4 授权回调域名配置的实操核对清单回调域名是授权环节里掉坑率最高的一个点。我总结了一份核对清单每次配环境都照着过一遍第一平台侧配置的回调地址必须与后续请求中携带的 redirect_uri 完全一致一个字符都不能差。协议、域名、端口、路径全部算。第二本地监听服务别用动态端口固定一个端口可以减少配置项。比如固定8899回调地址就是http://localhost:8899/callback。第三回调地址进.env配置不要在 SKILL.md 里写死。不同机器、不同环境用的地址可能不同写死在文档里基本就是坑后来人。第四OAuth 授权码是一次性的拿到之后要马上换 token。如果换 token 过程中网络中断这个授权码就作废了需要重新走一遍授权流程。脚本里最好对这种失败有明确提示。我踩过的真实例子本机跑得好好的切到服务器上授权全部失败查了半天是平台侧的回调域名只配置了 localhost没配服务器的内网地址。后来所有环境统一用127.0.0.1:8899 SSH 端口转发解决才把这个坑填平。理解这个机制之后你会发现回调域名的本质是“平台验证请求来源”的一道锁它不认你的域名漂移只认精确匹配。4.5 运行效果与验证方式配置完成之后我习惯先手动跑一遍验证链路。第一步执行python scripts/auth.py --status确认输出为active第二步手动调用一次发布接口确认 HTTP 200第三步删掉本地 token 文件再跑发布脚本确认它能够自动跳转授权或者给出清晰的错误提示而不是抛一个晦涩的异常。验证完这三步之后再打开 AI 助手测试技能。给 AI 一条指令“把 article.md 发布到平台”看它是否严格按照 SKILL.md 的流程先检查再发布。如果 AI 跳过了检查直接调接口你需要回头检查 SKILL.md 的措辞——描述得越具体AI 按流程走的概率越高。5. 常见问题与排查速查表授权管理现场实录5.1 五类翻车现场对照表整理一下我在实际项目里见过的授权相关报错。下面这张表可以直接收藏遇到问题照着对典型现象常见原因处理思路接口返回 401 Unauthorizedtoken 过期、token 未携带或格式错误检查请求头是否带了 Bearer token执行刷新脚本后重试接口返回 403 Forbiddentoken 有效但权限不足检查应用文档里申请的 scope确认授权范围授权回调报 redirect_uri 不匹配平台配置、请求参数与本地监听地址不一致逐项核对协议、域名、端口、路径精确匹配授权码换 token 失败授权码已过期或已使用过重新发起授权流程拿到授权码后立即换 token技能文件描述了授权但 AI 不执行检查SKILL.md 描述不够明确AI 自行跳步把“先检查再执行”写为强约束步骤甚至可以要求先跑状态命令这里有一个容易被忽略的点401 和 403 在排查思路上完全不同。401 是“你是谁”的问题解决方向是凭证403 是“你能做什么”的问题解决方向是授权范围。我看到过有人为了一个 403 反复刷新 token 刷了半天实际上换个 scope 就解决了。报错本身就告诉了你问题在哪一层别混淆。5.2 排查三板斧分开验证看见真相授权问题最怕的是在 AI 对话里来回折腾因为 AI 的上下文里没有运行时日志容易靠猜。我的经验是拆开验证按三层来排查第一层验证凭证层。单独用 curl 拿环境变量里的 token 请求接口比如curl -H Authorization: Bearer $PUBLISH_API_TOKEN https://api.example.com/v1/me。如果这条命令本身返回 401说明问题在 token 本身跟 SKILL.md、跟 AI 都没关系。第二层验证脚本层。直接运行python scripts/auth.py --status和python scripts/publish.py脱离 AI 环境跑一遍。脚本能跑通说明运行时逻辑没问题跑不通直接在脚本层修不要带着 AI 一起修。第三层再回到 AI 层。前两层都正常后再让 AI 执行技能重点观察它有没有按照 SKILL.md 描述的步骤操作。这一步如果失败多半是文档描述和实际行为有偏差调整措辞比改代码效率更高。这个顺序我屡试不爽。它把“我的代码坏了”和“我的 AI 不会用代码”这两件事彻底分开排查时间能砍掉一半以上。5.3 必须保持的好习惯幂等检查与优雅降级授权检查这件事我强烈建议做成幂等的。意思是说无论执行多少次同样的输入得到同样的结果不会因为重复运行而搞坏状态。设计--status命令时它不应该修改任何文件只读 token 并判断状态--refresh虽然会改 token 文件但在 token 仍然有效时不强制刷新避免没必要的请求。优雅降级对应的是失败场景。当授权失效时技能不应该抛一个“授权失败”的裸错误更不应该尝试绕过授权而是应该引导用户重新走一遍授权流程。SKILL.md 里明确写上“失败后提示用户运行授权命令”比让 AI 自己尝试各种组合要安全得多。直白点说AI 遇到 401 时的最优策略是“告诉人而不是自己折腾”。5.4 我在这些坑里摔出来的三条经验第一条经验是关于“环境变量没传递”的坑。AI 助手启动时有些 IDE 插件不会自动加载 Shell 的.env文件。明明终端里手动跑脚本好好的AI 一调用就报环境变量不存在。解决方式是在auth.py里用python-dotenv显式加载.env或者在技能启动前让用户确认环境已配置。别看这个问题简单它浪费了我整整一个下午。第二条经验是关于“授权文件被技能目录带进了仓库”。有一次我顺手把 token 文件放到了项目目录下提交代码时被 git 缓存带走了三分之一的平台访问令牌还好及时发现并紧急轮换。从此之后所有 token 一律放到项目外目录并在.gitignore里额外加了一条**/token*.json做双重保险。这个习惯至今没再踩雷。第三条经验是关于 SKILL.md 里描述授权时的措辞尺度。写得模糊AI 会自作主张有一次它为了“帮助我”获取 token试图猜解环境变量名写得过细比如在文档里列出了完整的 curl 带 token 的示例会导致 AI 用户在不经意间把 token 打印到日志里。我现在的平衡点是在文档里提及凭证变量名和脚本入口但绝不展示任何真实格式的 token 示例这样既给 AI 足够的行为约束又不让它有机会复述敏感内容。6. 一点个人体会写不下的东西本来就该活在运行时里折腾过一轮之后我把“SKILL.md 里写不下授权”这件事看得越来越明白它不是一个篇幅问题而是一个范式的错位。说明书写得再长也替代不了执行者的手SKILL.md 描述得再详细也替代不了授权服务器的回调与刷新逻辑。把不该它承担的责任还给它把不适合它承载的数据移走整个技能才会变得又稳又安全。我现在设计任何“需要授权”的 AI 技能都会先问自己三个问题凭证放在哪里、授权失败后用户怎么恢复、AI 在本环节里到底需要知道多少。前两个问题的答案永远在运行时最后一个问题的答案通常只需要一行文字。如果你也在写技能文件不妨把“授权”留给脚本和密钥管理系统把“如何决策”留在 SKILL.md。你会发现它不但写不下了而且根本不需要写下来。