如果你在 Claude Code 这类 Agent 工具里维护过技能文件你一定遇到过这种场面一个技能要调第三方接口运行前必须先过授权于是你开始往 SKILL.md 里疯狂补充授权说明——token 从哪来、有效期多久、要带什么 Header、失效之后怎么办……结果写了几百行模型该报 401 还是报 401。今天聊聊 SKILL.md 和授权之间的那层错位为什么你把授权写进 SKILL.md 本身就是个思路问题以及我把授权逻辑彻底搬出 SKILL.md 之后的一整套做法。这个问题的杀伤力在于它特别隐蔽。SKILL.md 看起来就是个普通文档你写点说明进去好像天经地义。但实际跑起来你就会发现文档是静态的授权是动态的——你把一个动态的、有状态的、依赖环境和时间的系统硬塞进一段静态文本里写不写不下只是表象真正的问题是这套组合从根上就不成立。1. SKILL.md 到底是给谁读的先别急着当配置文件用1.1 模型读 SKILL.md不是为了逐字执行你的授权备忘录Agent Skills 里的 SKILL.md核心设计意图就是让模型在一堆技能里快速判断这个技能要不要用、怎么用。它的头部结构很轻量一般就是技能名称、触发描述正文是执行步骤和注意事项。模型拿到一份 SKILL.md做的是意图匹配和路径规划而不是把里面每一句都当成运行时指令去执行。这意味着什么呢你把授权的内容写进去模型确实能读到但它读到的只是一个描述它没法验证这个描述在当前环境里是否成立。比如你写系统使用 Bearer Token 调用接口模型并不知道此刻环境变量里到底有没有这个 Token、这个 Token 是不是已经过期、当前登录的人对这个接口到底有没有权限。SKILL.md 不会替你去读取运行时状态它只是文本。我一开始也没想明白这一点总觉得把授权写全面点模型在处理任务的时候就能自己处理授权问题。后来我才意识到模型的所谓自己处理是从文本中推断出一个行为路径而不是真的去探测环境状态。它很可能因为文中一句密钥在配置文件中就尝试去读一个不存在的文件也可能因为一句授权已配置就直接跳过检查然后带着一个坏 Token 去调接口白白返回 401。打个比方SKILL.md 就像菜谱上写的本菜需要鸡蛋它没法替你回答现在冰箱里的鸡蛋是不是坏了。授权问题与此类似能不能调用不是由菜谱决定的而是由厨房的实际库存决定的。1.2 写不下的本质三种典型溢出场景很多人说SKILL.md 里写不下授权我后来总结了一下其实不是文件大小限制而是有三类信息注定塞不进静态文本密钥轮换频繁。有的平台每隔一段时间就强制吊销旧密钥或者你个人出于安全考虑主动轮换。你上次写进 SKILL.md 的 Key 可能已经失效了你得经常改文件改完还可能同步不到团队的共享仓库。多环境多账号差异。本地开发一套 Token、测试环境一套 Token、生产环境一套 TokenSKILL.md 是一份静态文本它没法同时表达在不同环境该用哪一套凭证。硬要写只能写成一段阅读理解题让模型去猜。交互式授权过程。不少平台的授权不是简单给个 Key 就完事而是需要跳浏览器、扫码、点确认、输验证码。SKILL.md 是纯文本你再怎么描述这个交互模型也没法替你完成这个动作更没法把一次性的授权结果固化在文档里。一旦你在这三种场景里尝试把授权写进 SKILL.md你就会发现文件越写越长但模型表现越来越不稳定。写到最后你其实是在用文档手写一个本应由程序处理的运行时逻辑。2. 授权信息千奇百怪哪些注定不该出现在 SKILL.md 里2.1 先按生命周期给授权分个类不是所有授权都不能碰 SKILL.md关键在于授权信息的形态。我习惯按生命周期先分个类这样判断起来很直观授权类型常见形态生命周期适合写进 SKILL.md 吗长期静态密钥API Key、Access Key数月到数年不适合存值只适合引用环境变量名短期动态令牌OAuth Token、临时 STS 凭证分钟到小时完全不适合必须运行时获取许可证/授权码软件 License、激活码绑定机器或有时限不适合应放授权管理服务或环境变量权限范围声明Scope、角色、所需权限项长期稳定适合描述技能需要什么权限环境绑定软授权加密狗、MAC 绑定长期但绑定设备不适合应由外部程序探测从这个表格能看出来真正适合放进 SKILL.md 的只有权限范围声明这一类。因为它是技能本身的属性——我这个技能需要 read:report 权限——这是一个稳定的声明描述的是技能的边界写进去反而能让使用者在配置授权时有据可依。其他类型的授权要么是动态的要么是敏感值要么是绑定环境的都不应该出现在技能描述文件里。你把 API Key 的值写进去等于把密码贴在了技能说明书的第一页你把 OAuth 流程写进去等于让一个读文档的人去假装自己是 OAuth 客户端。2.2 一个简单的判断标准静态知识还是运行时状态我后来给自己定了一条很硬的标准遇到任何要不要把这个信息写进 SKILL.md的纠结就问一句这是静态知识还是运行时状态静态知识指的是技能的功能边界、输入输出约定、调用时机、触发条件。比如当用户要求生成月度报表时使用本技能输出格式为 CSV需要 read:report 权限。这类信息稳定、不依赖具体某台机器的状态写进 SKILL.md 完全合理。运行时状态指的是当前凭证值、当前 Token 是否有效、当前环境是否已授权、授权过期时间。这类信息是时刻变化的只有程序运行时才能探测文本无法承载。按这个标准你回头看那些写不下的授权内容基本都属于运行时状态。你把这个状态硬写成文本它只会出现在一个错误的时间点以错误的准确度呈现给模型。所以不是 SKILL.md 放不下而是它本来就不该放这些。3. 我硬把授权塞进 SKILL.md 之后踩的三个坑3.1 密钥直接写进 SKILL.md第二天就被扫描工具盯上这个坑是我印象最深的。那是给内部知识库写一个检索技能我图省事在 SKILL.md 正文里直接贴了一个内部 API 的 Key想着反正是内部服务不会有问题。结果代码库被安全扫描工具例行检查的时候直接报检测到疑似凭证泄露整个 MR 被卡住团队组长直接来找我谈话。当时我还挺委屈觉得内部接口而已。后来复盘才反应过来SKILL.md 这种文件特别容易被传播——它可能被同步到团队共享仓库可能被复制到示例目录也可能在分享技能的时候被直接发给别人。你把 Key 写在里面等于把钥匙焊在门上路过的每个人都能看到。处理过程很狼狈吊销原 Key、全仓库搜索残留、重写提交历史之后好几天团队的 CI 都在等密钥重置完成。从那以后我立了一条规矩任何形式的密钥值永远不写进 SKILL.md、README、示例文档、提交信息里。文档里最多出现环境变量名比如REPORT_API_TOKEN让读者知道这里应该配一个东西而不是把东西本身摆出来。3.2 永久授权也会过期SKILL.md 里的死值瞬间变废纸还有一次我用了一个第三方报表服务的密钥对方文档里写永久有效我也真信了就在 SKILL.md 里写了一段API_KEYxxxx永不过期。结果用了大半年某天接口突然开始 401我第一反应是去翻 SKILL.md 确认密钥上面白纸黑字写着永不过期完全看不出问题。排查了很久最后登录对方后台才发现平台因为安全合规要求把所有旧批次的 API Key 全部吊销了跟永久两个字毫无关系。那一刻我就明白了你在 SKILL.md 里写下的永久只是你某个时间点的理解快照不是系统的真实约束。好在后来我把授权检查逻辑从 SKILL.md 里挪到了脚本里再遇到这种情况只需要在脚本对应的配置里更新 Token或者让脚本去访问一个授权服务重新拉取一分钟搞定。SKILL.md 本身根本不用动也不会因为一个过期的 Key 误导模型的判断。3.3 OAuth 流程描述得太细模型反而不知道该干什么第三个坑最有意思。当时做一个新技能需要走标准的 OAuth2 授权码流程我担心模型不知道流程就把整个授权流程按步骤写进了 SKILL.md从引导用户打开授权链接到接收回调、换取 Token写了好几段自认为非常全。结果模型的表现极其分裂。有时候它上来就对我说我可以帮你打开浏览器授权有时候它又直接尝试去调用 Token 接口还有一次它把授权链接当成了普通 URL 去请求拿到 HTML 页面后一脸茫然。为什么因为我在 SKILL.md 里塞进去的信息太多而且这些信息指向了多个不同的动作方向模型在当前这一步到底该由谁来做上产生了分歧。后来我把交互细节全部从 SKILL.md 撤掉改成脚本里明确的分支逻辑如果检测到没有 Token就打印授权指引并停止执行如果检测到 Token 存在就正常调用接口。模型只需要读脚本的输出照着把信息转述给用户。行为立刻稳定了。4. 把授权搬出去之后我的四层落地做法4.1 第一层环境变量是所有凭证的第一道边界环境变量是存放运行时凭证最优的基础方案原因很简单进程启动时读取、运行时内存持有、不进入 Git、可以被 CI 平台或本地 shell 独立注入。只要你不在 SKILL.md 里写死值而是约定这些东西放在环境变量里授权信息就有了一个真正属于它的位置。我在每个技能项目的根目录维护一个.env.example里面只写变量名和说明不写真实值# .env.example —— 复制为 .env 并填入真实值 REPORT_API_TOKEN REPORT_API_SCOPEread:report.env加入.gitignore永不提交。SKILL.md 和所有脚本只引用REPORT_API_TOKEN这个变量名不出现真实值。这样就算谁的 SKILL.md 被到处传泄露的也只是一个名字而不是凭证本身。4.2 第二层独立的授权预检脚本把猜变成查有了环境变量之后下一步是让运行过程能主动检查授权状态而不是让模型从文档里猜。我习惯为每个需要授权的技能配一个预检脚本比如scripts/check_auth.py它在执行主流程之前先做探测。这里给一个可以直接改着用的模板#!/usr/bin/env python3 import os import sys import requests def main(): token os.environ.get(REPORT_API_TOKEN) if not token: print(未检测到 REPORT_API_TOKEN请先执行 cp .env.example .env 并填入凭证) sys.exit(1) try: resp requests.get( https://api.example.com/auth/check, headers{Authorization: fBearer {token}}, timeout5, ) except requests.RequestException as exc: print(f授权服务不可达{exc}) sys.exit(2) if resp.status_code 401: print(REPORT_API_TOKEN 已失效请重新生成并更新 .env) sys.exit(3) print(授权状态正常可执行后续任务) sys.exit(0) if __name__ __main__: main()这个脚本的关键设计是区分不同的错误码退出码 1 表示没配置、2 表示服务不可达、3 表示凭证失效。每个错误码对应一句明确的修复指引。Agent 读到退出码和提示信息后不需要自己猜测授权状态只需要把提示贴给用户或按提示引导用户处理。这样模型的行为就完全可控了。4.3 第三层启动时自动引导授权而不是在 SKILL.md 里描述授权对需要交互式授权的服务我可以接受 SKILL.md 里完全不提怎么授权这件事因为授权引导应该由预检脚本来输出。脚本在检测到授权缺失时可以直接打印出用户当前需要做的事情比如# 检测到 Token 缺失输出引导 请打开 https://api.example.com/device 并输入授权码 ABC-123 授权完成后脚本将自动继续执行这种做法的好处是交互的信息由程序实时生成授权码也是动态的模型只是把这段输出原样转述给用户。SKILL.md 里不需要描述如何完成一次设备授权那些步骤和状态都在脚本里被实时处理了。4.4 那 SKILL.md 里到底还写什么一段可以直接参考的写法搬走授权逻辑之后SKILL.md 会变得非常薄但反而更精确。我现在的写法是这样的--- name: report_billing description: 生成账单报表。仅当 REPORT_API_TOKEN 授权可用时使用运行前必须执行 scripts/check_auth.sh 预检。 --- ## 触发场景 - 用户要求生成本月账单报表 - 用户要求导出销售汇总 ## 执行步骤 1. 执行授权预检bash scripts/check_auth.sh 2. 预检通过后调用内部账单服务生成报表 3. 若预检失败将 check_auth.sh 输出的提示原文转述给用户 ## 授权说明 需要环境变量 REPORT_API_TOKEN权限范围 read:report注意 description 里写了仅当授权可用时使用和运行前必须执行预检这是给模型的触发约束正文里的授权说明只声明需要什么权限和变量不解释怎么获取、怎么刷新、当前是否有效。这些运行时信息全交给了脚本。这样 SKILL.md 既保留了技能边界描述又不承担授权状态管理模型读起来也就不会跑偏。5. 团队协作时授权这件事比单机更要命5.1 一把万能 key 引发的血案单机场景下你管理自己的 Token 还算容易。团队协作后问题复杂度直线上升。最典型的翻车现场就是为了省事所有人共享同一个 API Key谁都能调用所有接口。结果一个成员本地脚本有 bug误调了生产接口追查的时候发现权限根本分不清是谁干的因为大家用的都是同一个身份。我现在的原则是不要让任何 SKILL 依赖共享万能 Key每个环境、每个角色尽量有独立凭证。虽然配置起来麻烦一点但出问题后能快速定位到人权限边界也清晰。5.2 .env.example 模板与密钥隔离一起用团队共享 SKILL.md 和脚本的时候这些文件本身是可以入库的但.env文件绝对不能入库。正确做法是只提交.env.example作为模板新成员拉代码后先复制一份.env再填入自己申请的 Token。这个流程看起来简单但少了.env.example模板还是容易乱。模板文件是团队约定的一部分它告诉每个成员这个技能需要哪些变量、每个变量的用途是什么。有模板在新人五分钟内能自己配好环境没模板新人就得去翻 SKILL.md 猜猜着猜着就把真实 Key 写进文件里去了。5.3 CI/CD 里的静默授权没有交互界面怎么过预检CI/CD 环境比较特殊没有交互终端没法让用户扫码或输验证码。这时候我一般用服务账号加临时令牌的机制CI 平台的 secrets 管理里存放服务账号的 Token构建时通过环境变量注入。# .github/workflows/billing.yml 片段 steps: - name: 注入临时凭证 run: | echo REPORT_API_TOKEN${{ secrets.REPORT_API_TOKEN }} $GITHUB_ENV - name: 授权预检 run: python scripts/check_auth.py关键点在于预检脚本在 CI 和本地可以复用同一份代码只是 Token 来源不同。脚本不关心 Token 是怎么注入的它只关心环境变量里有没有、是否有效。这样本地、CI、生产环境的行为完全一致不会出现本地能跑、CI 报 401的玄学问题。5.4 权限最小化与定期轮换别让一个 Token 管所有接口团队里我还会做两件事一是给每个技能只申请必要的最小权限范围。比如一个技能只读报表那就只给它read:report不给write:report。这能防止一个技能被误用时造成不可逆操作。二是定期轮换凭证比如每 90 天强制换一批 Token。轮换的执行也不复杂预检脚本里检测到 401 时输出提示请重新生成凭证并更新 .env。这样就算有人忘了换Agent 在运行技能时也会因为预检失败而明确提醒而不是带着旧 Token 一路撞墙上最后给你一个莫名其妙的接口报错。6. 我现在的最终结构目录、文件与一段可直接抄的 SKILL.md6.1 目录结构把授权相关的全部收进 scripts/我最后沉淀下来的技能目录长这样skills/ report_billing/ SKILL.md scripts/ check_auth.py generate_report.py customer_search/ SKILL.md scripts/ check_auth.py search.py .env.example .gitignore这个结构的核心逻辑是SKILL.md 只负责描述技能边界和触发条件scripts/ 负责所有运行时逻辑包括授权预检、凭证读取、接口调用。这样划分之后SKILL.md 变得很薄很稳定几乎不需要频繁改动真正的动态变化全部集中在脚本和环境中出了问题也只动脚本和环境变量不碰技能描述文件。6.2 一段完整的 SKILL.md 示例可直接抄--- name: report_billing description: 生成账单报表。仅当 REPORT_API_TOKEN 授权可用时使用运行前必须执行 scripts/check_auth.py 预检。 --- ## 触发场景 - 用户要求生成本月账单报表 - 用户要求导出销售汇总 ## 执行步骤 1. 执行授权预检python skills/report_billing/scripts/check_auth.py 2. 预检通过后调用内部账单服务生成报表 3. 若预检失败将 check_auth.py 输出的提示原文转述给用户不尝试自行修复 ## 授权说明 需要环境变量 REPORT_API_TOKEN权限范围 read:report ## 输出规范 - 报表生成后输出 CSV 格式包含日期、金额、订单号这份结构里模型需要做的决策非常少要不要用这个技能、预检脚本输出什么就转述什么、生成报表后按格式输出。授权状态完全由脚本决定模型不需要理解 Token 从哪来、怎么刷新、什么时候过期。6.3 我沉淀下来的几条原则值不进文本任何密钥的值永远不写进 SKILL.md、README、提交信息文档里只允许出现环境变量名。名字进文本环境变量名和权限范围名可以写进 SKILL.md让使用者和模型知道要配什么但不暴露具体凭证。检验进脚本有没有授权、Token 是否有效、该走哪个授权流程全部由预检脚本探测和输出不由模型从文档里推断。交互留给用户需要跳浏览器、扫码、输验证码的脚本输出指引Agent 转述即可不把交互流程写进 SKILL.md。最后说一个我现在坚持的习惯每当我要给一个技能增加新权限都会先问自己——这个信息是稳定的技能边界还是随时会变的运行状态前者进 SKILL.md后者进脚本和环境变量。按这个标准执行之后SKILL.md 变得很薄但 Agent 任务成功率反而高了很多至少那些莫名其妙的 401 再也没回来找过我。