005、权限系统深度设计:deny、prompt、allow 三级策略与 allowlist 构建
005、权限系统深度设计deny、prompt、allow 三级策略与 allowlist 构建从一次“权限黑洞”调试说起上周五凌晨两点我盯着终端里Claude Code反复报出的“Permission denied”错误咖啡已经凉透了。问题出在一个看似人畜无害的deny规则上——我写了个deny all然后试图用allow覆盖某个子目录结果Claude Code直接罢工了整整三小时。后来翻源码才发现deny的优先级设计和我直觉完全相反它不是“先拒绝再放行”而是“拒绝即终局”。那次之后我花了整整一周重构了整个权限系统。今天这篇笔记就是那周踩坑的完整记录。三级策略别被名字骗了很多人看到deny、prompt、allow三个级别第一反应是“这不就是白名单、黑名单、灰名单吗”——错。这三者的设计哲学完全不同它们不是并列关系而是拦截链上的三个关卡。deny沉默的终结者deny规则一旦匹配直接返回403不会继续往下检查prompt或allow。这是最容易被误解的地方。# 别这样写——deny all 会吞掉所有后续规则deny:-**/*.env-**/credentials*# 正确姿势deny只放最确定的禁区deny:-**/secrets/**-**/.ssh/**-**/id_rsa*这里踩过坑如果你在deny里写了**/config/*然后试图用allow放行config/prod.yamlClaude Code会直接无视allow——deny的匹配是短路逻辑一旦命中就结束。prompt最容易被滥用的“中间人”prompt的设计初衷是“不确定时问用户”但实际使用中很多人把它当成了“懒得写规则时的垃圾桶”。这是大忌。# 反面教材——prompt 太多会导致用户疲劳prompt:-**/*.yaml-**/*.json-**/logs/**# 正确用法——只对高风险但非绝对禁止的操作弹窗prompt:-**/deployments/**# 部署相关需要确认-**/terraform.tfstate*# 状态文件确认是否真的需要修改prompt的触发频率需要严格控制。我个人的经验阈值是单个会话中prompt弹窗不超过3次。超过这个数用户就会开始无脑点“允许”prompt就失去了意义。allow最后的守门员allow是三级策略中唯一能覆盖deny的规则但有个前提deny不能是deny all。如果你写了deny allallow就彻底失效了。# 正确的allow设计——精确到文件级别allow:-src/**/*.ts-src/**/*.tsx-tests/**/*.test.ts-docs/**/*.md-!docs/internal/**# 排除内部文档注意那个!前缀——这是allowlist里的排除语法相当于在allow内部又做了一层deny。这个设计很巧妙但容易让人晕。我习惯把它叫做“allow内的暗桩”。allowlist构建从“能做什么”到“应该做什么”很多团队的权限配置停留在“能做什么”的层面比如“允许读写src目录”。但真正工程化的allowlist应该回答“应该做什么”。基于角色的allowlist模板# 开发者角色developer:allow:-src/**/*.{ts,tsx,js,jsx}-tests/**/*-docs/**/*.md-package.json-tsconfig.jsondeny:-**/node_modules/**-**/dist/**-**/.env*prompt:-src/deployments/**-src/config/production/**# 运维角色ops:allow:-deploy/**/*-k8s/**/*-terraform/**/*-**/Dockerfile-**/docker-compose*.ymldeny:-src/**/*.ts# 运维不需要碰业务代码-**/node_modules/**prompt:-**/secrets/**-**/credentials/**这里有个细节角色之间不要有重叠的allow规则。如果developer和ops都能写deploy/目录一旦出现权限问题排查起来就是噩梦。我见过最离谱的案例是两个角色的allow规则互相覆盖导致某个文件既被允许又被拒绝Claude Code直接崩溃。allowlist的版本管理allowlist不是写一次就完事的。随着项目演进目录结构会变新工具会引入旧规则会失效。我建议把allowlist当成代码来管理# v2.1.0 - 2024-03-15# 变更新增AI模型目录移除旧的ml_models目录allow:-src/**/*.ts-ai_models/**/*.h5# 新增-!ml_models/**# 旧目录不再允许每次变更都要写changelog不是为了给别人看是为了三个月后的自己还能想起来为什么改。实战中的“暗坑”坑1通配符的贪婪匹配**/*.ts和**/*.ts看起来一样但前者会匹配任意深度的.ts文件后者只匹配当前目录。这个区别在嵌套项目结构里会要命。# 错误——只匹配根目录的.ts文件allow:-*.ts# 正确——匹配所有子目录allow:-**/*.ts坑2deny规则的顺序依赖deny规则的顺序不是无关紧要的。Claude Code会按顺序匹配一旦匹配就停止。# 危险——先deny了所有后面的规则永远匹配不到deny:-**/*-!src/**# 这个永远不会生效# 正确——从具体到通用deny:-**/secrets/**-**/.env*-**/credentials*-**/node_modules/**坑3prompt的“确认疲劳”我见过最极端的案例一个项目配置了20多个prompt规则每次Claude Code执行任务都要弹窗5-6次。结果用户直接写了个脚本自动点击“允许”prompt形同虚设。解决方案prompt规则数量控制在5条以内且每条都要有明确的触发条件。比如“只有修改production配置时才弹窗”而不是“所有配置文件都弹窗”。个人经验权限系统的“二八法则”做了这么多权限系统我总结出一个规律80%的安全问题来自20%的规则配置。具体来说deny规则越少越好。每多一条deny就多一个潜在的死锁点。我现在的项目deny规则不超过10条。allow规则要精确到文件级别。不要写src/**这种粗粒度规则要写src/**/*.ts。粗粒度规则是安全漏洞的温床。prompt是最后的手段。能用deny解决的不要用prompt能用allow解决的不要用prompt。prompt只用于“需要人类判断”的场景。定期审计allowlist。每两周检查一次删除不再需要的规则更新过时的路径。这个习惯救了我好几次。最后说一句权限系统不是越严格越好而是越精确越好。一个精确的allowlist比一百条deny规则更有效。Claude Code的权限设计哲学就是“最小权限原则”——只给AI它真正需要的权限不多不少。下次遇到权限问题先检查你的deny规则是不是写得太宽了。大概率是。