qings-pi-modes1.4.0
Declarative, user-definable permission modes for the pi coding agent: OS-level sandboxing (bubblewrap / sandbox-exec), an allow/ask/deny policy engine, real bash AST gating (tree-sitter), tool hiding, and skill/custom-tool gating.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
qings-pi-modes
Qing 的 pi 权限模式 —— 基于 wynainfo/pi-permission-modes 的 fork,重构为 CC 式五模式 + 可解耦的三态边界沙箱 + 独立网络轴 + AI 审批模式。
为 pi coding agent 提供可切换、可自定义的权限模式。每条轴独立可控:
| 轴 | 快捷键 | 内容 |
|---|---|---|
| 审批模式 | alt+m |
谁来批准 ask:人工 / AI 分类器 / 自动接受 |
| 沙箱边界 | alt+s |
OS 级沙箱的三态:只管「工作目录之外的读写」 |
| 网络 | alt+n |
域名过滤:filtered ⇄ open(独立于沙箱与模式) |
三条轴完全正交:切模式不重启沙箱、切沙箱不改审批、切网络不影响其它——footer 状态条是唯一事实来源。
bash 门控使用真正的 tree-sitter AST(不是正则),OS 沙箱由
@anthropic-ai/sandbox-runtime(Linux 用 bubblewrap)提供。
见 SECURITY.md(威胁模型)、CHANGELOG.md(变更)、 CONTRIBUTING.md(参与开发)、规划文档(设计决策)。
安装
npm(推荐)
pi install npm:qings-pi-modes
pi install -l npm:qings-pi-modes—— 项目级安装(.pi/内,可与团队共享)pi -e npm:qings-pi-modes—— 不写入配置,单次试用
git(跟随仓库)
pi install git:github.com/qingshanblue/qings-pi-modes
pi install 会自动执行 npm install(带入 @anthropic-ai/sandbox-runtime、tree-sitter 等运行时依赖);pi SDK 作为 peer dependency 由宿主提供。两种来源通用:
pi update --extensions—— 之后拉取更新pi list/pi remove <source>—— 查看 / 卸载
本地开发:
pi -e ./src/index.ts
系统依赖
Linux(含 WSL2) 需要 PATH 上有三个包:
sudo apt install -y bubblewrap socat ripgrep
bubblewrap—— 提供bwrap可执行文件,用于约束 bash(包名bubblewrap,可执行文件bwrap);socat—— 网络过滤;缺失时沙箱初始化失败,沙箱模式降级为弹窗;ripgrep—— 提供rg。
Ubuntu 24.04+(桌面/服务器;WSL2 镜像通常没有)默认启用
kernel.apparmor_restrict_unprivileged_userns,会剥夺 bubblewrap 与运行时 seccomp 助手所需的能力,所有沙箱命令都会报bwrap: ... Operation not permitted。 处理:sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0(持久化到/etc/sysctl.d/),或在 AppArmor 配置里给bwrap授予 userns。 WSL1 完全没有 user namespaces,沙箱会报告不支持。
macOS 使用内置 sandbox-exec,无需额外安装。
原生 Windows:无沙箱。 扩展不做 Windows 沙箱,沙箱模式降级为仅弹窗: 项目内
bash一律询问确认,网络 allowlist 不生效,allowWrite只参与提示 边界。需要真正的隔离请用 WSL2 并按 Linux 说明操作。
验证
- 在任意项目启动 pi —— footer 显示模式指示,如
Ask; - 切到 Auto(
alt+m直到Auto,或/permission auto):- 沙箱可用 → footer 显示
Edits box:on; - 否则 →
Edits box:unavailable+ 修复提示;
- 沙箱可用 → footer 显示
/sandbox打印当前沙箱配置(或不可用的原因)。
三条轴
① 模式(审批自动化级别)
alt+m 循环(按权限从低到高),/permission <mode> 直达,/permission 弹出选择器。当前模式随会话持久化
(/reload、resume、分支导航都保留),显示在 footer。
| 模式 | 行为 |
|---|---|
plan ⏸ |
规划模式。读免费;只有 Markdown(*.md/*.markdown)能在项目内创建/编辑,其它 edit/write 阻止。变更类 bash 会弹窗询问(只读命令走白名单免问)。系统提示引导模型:把计划写到 plan/<YYYY-MM-DD>_<描述>.md,用 show_plan 渲染给你审,然后交接:出现 Accept / Decline 卡片(见批准计划)。 |
ask ● |
每个变更类操作都问你:bash/edit/write/web 搜索/扩展工具。读免费;白名单内的只读 bash 免问。批准的项目内 bash 在沙箱内运行。 |
edits ✎ |
项目内 edit/write 不再确认(= Claude Code 的 accept-edits);bash 仍逐条询问。项目内 bash 在沙箱内运行。 |
auto ▶ |
项目内读/写/改全自动;白名单 bash(npm run/test、git add/commit、make、pytest)全自动;其余 bash、项目外读、扩展工具交给 AI 安全分类器(见 AI auto 模式);会脱离沙箱的逃逸(写越界 / 提权)仍走人工。 |
accept ⚡ |
从不询问(自动接受所有策略决定)——但仍受沙箱边界约束:alt+m 决定它能触达什么。无保护路径兜底。 |
- 循环顺序 =
plan → ask → auto → accept(alt+m)。edits不在循环中,用/permission edits直接切换;/permission的用法提示列出全部模式。 - 模式是数据:可在
permission-mode.json里调整或新增(见配置)。
② 沙箱边界(三态,只管「工作目录外」)
沙箱只有一个职责:项目边界。alt+s 循环三档(/sandbox off|on|strict 直达,/sandbox status 看详情):
| 档 | 外路径读 | 外路径写 | 项目内 | 越界处理 |
|---|---|---|---|---|
| off | ✅ | ✅ | 原样 | 不做逃逸检测(审批策略仍然生效) |
| on(默认) | ✅ | ⛔ OS 拦 | 原样 | 只读越界:仍在沙箱内,交策略/分类器;写越界/提权:弹窗(批准后脱离沙箱执行) |
| strict | ⛔ | ⛔ | 原样 | 直接 block,不弹窗(见下) |
strict的「外读也拦」由策略层实现(dispatcher 对越界访问直接拒绝)—— bubblewrap 必须让 bash 读系统库(/usr、/lib…),OS 层无法把项目外的读全部 遮蔽,否则 shell 本身都跑不起来。对模型而言效果等同:碰不到外路径。off不改变 in-bounds 集合:scratch 目录与/tmp/pi仍算"项目内", 文件工具不会把它们当越界(沙箱off= 一切照旧,只是没有 OS 约束)。- 会话状态持久化(
/reload、resume、分支导航都保留),并转发给子 agent (PI_PERMISSION_SANDBOX);headless 子进程无转发值时默认on。 - 模式可用
sandboxPin钉住某档(覆盖alt+m,离开该模式恢复会话状态); 内置模式都不使用它,保留给自定义模式。
③ 网络(独立轴)
见网络。域名 allowlist 与「谁审批」「沙箱档」都无关,alt+n 随时切换。
④ AI 审批(auto 模式的闸门)
见 AI auto 模式。
批准一个计划
Plan 模式(任何 systemPrompt 为 "@plan" 的模式)里用 show_plan 渲染了计划后,
实施只需一步。其它模式里的 show_plan 只做展示。模型的交接话术一上屏,就会出现:
Plan ready: plan/2026-09-24_feature.md
> Accept: switch to Edits and implement it
Decline: keep refining in Plan Mode
- Accept 切换到批准模式(默认
edits),并发送一条真实的用户消息(默认The plan in \` is approved. Implement it now.,可用plan.approveMessage` 定制)——实施回合在 Edits 的系统提示下开始,而不是还挂在 Plan 提示下。这条消息 和手打的一样可见于记录。 - 批准只覆盖你看到的内容:计划文件在展示后被改动过,批准会被拒绝,
直到模型重新
show_plan。 - Decline(或 Esc)零成本:你留在 Plan 模式继续打磨。计划保持 pending,
下次
show_plan才会再次提供。
两条兜底路径覆盖其它入口:
- 计划 pending 期间手动切进批准模式(
alt+m、/permission edits)会问Implement plan/... now?;No 留在 Edits,什么都不发。 /plan approve直接切换并发送,不再询问;/plan status显示 pending 的计划。
pending 计划在 /reload、resume、分支导航后依然保留(perm-plan 会话条目;
已批准的计划不会再提供)。headless 会话没有弹窗、也不会自动切换。两个可选的
全局配置项:
"plan": {
"approveMode": "edits", // 批准后切到的模式(默认 edits)
"approveMessage": "The plan in `{path}` is approved. Implement it now." // {path} = 计划路径
}
项目配置不能设置它们。approveMode 指向不存在的模式时,批准功能关闭并给出一次性警告。
会话 scratch 目录
每个会话有自己的临时目录:Linux/macOS 为 /tmp/pi/<session-id>/,Windows 为
<os.tmpdir()>/pi/<session-id>/(可用 PI_PERMISSION_TMPDIR 环境变量覆盖 base,
例如 /tmp 挂了 noexec 时)。awareness 段落会提到它;bash 内的 TMPDIR
指向那里(沙箱内、沙箱外都生效;Windows 另有 TEMP/TMP);它始终
可写且在 in-bounds 内——即使全局或项目配置把共享的 /tmp/pi 从 allowWrite
里去掉,这句话依然成立。/sandbox 显示路径。
沙箱层面,宿主机上所有 pi 会话共享 /tmp/pi base(它在出厂 allowWrite 里;
base 以 sticky + world-writable 创建,类似 /tmp,各会话目录为 0700)。
指令层面,每个会话被告知只用自己的目录——包括 accept 模式(它从不询问,
但也会拿到一个简短的 scratch 指针)。Plan 模式对变更类 bash 走审批,所以
bash 写不进去(和其它模式一样)——但 Write/Edit 工具仍可使用它。
关机时不删除——/reload 与会话恢复能再次找到文件(目录以 pi 的 session id
为键)。改为在每次会话启动时,清理 7 天未动的兄弟目录;当前目录会在启动时
touch,长会话不会被并发会话清扫掉。base 下的文件与符号链接永不触碰。
网络
沙箱开启的模式里,bash 的网络流量按域名 allowlist 过滤(默认:npm、PyPI 及其文件 CDN、 GitHub 及其 CDN、Rust/Go/Ruby/Maven 包注册表、NixOS 缓存)。 这不是一堵静默的墙——访问 allowlist 之外的主机时会暂停并询问你 (Allow for session / Allow forever / Deny),然后放行或失败:
alt+n切换本会话的过滤:footer 上net:limit(黄)⇄net:free(绿);/network——status(allowlist、会话授权/拒绝)、allow <domain…>(会话级授权)、open/restrict(同alt+n)、reset(清空会话授权/拒绝);- 模型有
request_network_access工具:它列出域名与理由,你批准或拒绝—— 一次可覆盖多个域名(比如一次安装需要的所有主机)。被拒的主机本会话保持拒绝 (避免安装器反复重试造成的弹窗风暴),被拦的主机会在命令输出里报告, 模型不用猜代理发生了什么。 - "Allow forever" 把域名持久化到全局配置的 allowlist。把
network.askOnBlockedHost设为false可恢复静默拒绝。
授权/拒绝/开关都是会话级的("Allow forever" 除外),即时生效——不重启沙箱。
命令里写明的主机(如 curl https://…)在执行前就按 allowlist 检查:不在名单内的会
直接问你,永远不交给 AI 分类器;被拒的主机直接拦截;alt+n 的 open 状态同样放行。
无沙箱时这一检查仍然生效,因为 allowlist 是你的出站策略。
⚠️ 网络过滤由沙箱运行时的 socat 代理实现,因此沙箱
off时无法过滤 (footer 显示net:open)。这是运行时的硬约束。
AI auto 模式
auto 模式把 ask 级决定交给 AI 安全分类器,而不是人工弹窗——最接近
Claude Code auto mode 的形态。它是唯一带 "classifier": true 的内置模式。
审批流水线(每条 ask 依次经过):
硬拦 保护路径 / 策略 deny → 直接 block(AI 看不到)
Tier 0 permission.allow 规则 → 放行
Tier 1 只读命令白名单(内置) → 放行(无重定向/命令替换才合格)
Tier 2 其余 ask(含只读越界读) → 模型审批(默认仍在沙箱内执行)
旁路 提权 / 沙箱开启时的逃逸 / 出项目文件工具写入 → **始终人工**;沙箱 off·不可用的其余 bash → Tier 2 AI 审查(`aiGatesUnsandboxed`,出厂 auto 默认开启;strict 沙箱态下直接 block)
出厂 auto 的 Tier 0 已包含:write/edit、web_search、skill,以及
bash 命令族 npm run * / npm test* / git add * / git commit * / make * /
python -m pytest*——这些完全不经过分类器;其余 bash 与扩展工具首次使用走
Tier 2。沙箱 off/不可用时,可完整审查的非提权 bash 同样交 AI(出厂 auto 的 aiGatesUnsandboxed: true)。
想调整就改全局配置里 modes.auto.permission;想恢复「无沙箱一律人工」,设 aiGatesUnsandboxed: false。
分类器进程内运行,走 pi 的模型层(
ctx.modelRegistry),复用你真实的 请求鉴权(models.json、OAuth、环境变量密钥),无需额外配置。模型默认
"current"——跟随当前会话模型,/model切换它也跟着换,零配置。 用/classifier-model <provider/model>更换(/classifier-model current切回); 持久化到全局配置的classifier.model。看得到你的请求:审查时会把你最近一条消息(截断)一并交给分类器—— 否则它只看操作本身,无法区分"合法编辑"与"越权操作",会大量误拦。
Tier 1 白名单:已知只读命令(
ls、cat、grep、git status…)且无 重定向/命令替换 → 不调用模型直接放行。ask与edits共享这份白名单, 只读命令在哪都不烦你。逃逸永远不合格。 这份白名单刻意保守:能写文件或执行任意代码的写法一律不合格——包括find -execdir/-okdir/-fprint、sort -o、uniq 输入 输出、git … --output=等 隐藏写法;sed整体不在白名单(它的e命令与 GNU 的s///e会执行 shell)。 只读的grep -i/grep -o/sort -n仍然免弹窗;如果你有常用的只读sed, 用策略规则再放开(如"bash": { "sed -n*": "allow" })。fail-closed 默认:分类器出错/超时/返回不可解析 → 该操作被拦。全局配置
"classifier": { "failClosed": false }可改为降级人工弹窗。沙箱 off/不可用时的非提权 bash 交 AI(
aiGatesUnsandboxed,出厂 auto 默认true): 可完整审查的动作由分类器裁决;脚本引用其它文件、内容截断、不透明替换等无法完整审查的 payload 转人工。始终人工:提权、enforcing 沙箱的逃逸、保护路径(硬拦)、文件工具项目外写入。 这是“我信任这台机器”的取舍,不是安全边界。直接执行的项目内脚本(上限 8 KiB)会连同内容进入审查; 项目外脚本不会读入模型上下文,直接回退人工。自定义模式如需同样行为,显式设置:
"trusted-auto": { "classifier": true, "aiGatesUnsandboxed": true, "permission": { "bash": { "*": "ask" } } }只读的项目外访问不再弹人工:只读命令(Tier-1 白名单、无重定向/命令替换)读项目外时, 仍在沙箱内执行、交策略/分类器判定,而不是走「批准后无沙箱运行」。沙箱本来就允许项目外 只读(仅屏蔽
denyRead秘密),因此这既省一次人工、又少一次特权。它是启发式审查,不是安全边界:审查文本里含模型写的命令与参数,理论上可被诱导; 不可解析的回复一律按拒绝处理,但仍请把
classifier: true的模式当作便利过滤器—— 真正的边界是沙箱态(alt+s)与硬拦规则(详见 SECURITY.md)。连续拒绝达
classifier.maxConsecutiveDenials(默认 5)→ 转人工一次,避免卡死;拒绝按 surface 分别计数(一次写拒绝不会把无关的 bash 命令推过阈值),一次 allow 清零该 surface。footer 显示
AI:current-model(或AI:<模型>)。分类器对文件/扩展/网络等ask照常工作; 只有会脱离沙箱的bash在沙箱未生效(footerbox:off/box:unavailable)时转人工。
全局 permission-mode.json 中的分类器配置:
{
"classifier": {
"enabled": true,
"model": "current",
"timeoutMs": 15000,
"failClosed": true,
"maxConsecutiveDenials": 5
}
}
分类器配置仅全局——项目配置不能把审查者换成更弱的模型。要自定义 AI 模式,
给任意模式加 "classifier": true 即可。
快捷键
| 快捷键 | 作用 |
|---|---|
alt+m |
循环审批模式:plan → ask → auto → accept |
alt+s |
循环沙箱态:off → on → strict |
alt+n |
切换网络过滤:filtered ⇄ open |
命令
| 命令 | 作用 |
|---|---|
/permission [mode] |
设置模式(无参数:交互式选择器);/permission init 生成全局配置副本;clear-approvals、blocks、unblock <path> |
/classifier-model [current | <provider/model>] |
查看/切换 auto 审批模型(持久化) |
/plan approve · /plan status |
批准并实施 pending 的计划 / 查看 |
/network status · allow <domain…> · open/restrict · reset |
网络过滤(无参数:交互式选择 limit/free);会话状态与授权 |
/sandbox [off|on|strict|status] |
设置沙箱边界档(无参数:交互式选择器);status 看详情 |
启动旗标:--perm <mode>(指定初始模式)、--no-sandbox(禁用 OS 沙箱,
降级为 AST/启发式门控 + 弹窗)。
保护是如何工作的
两层独立机制叠加:
- 策略引擎(
allow/ask/deny):每次工具调用按当前模式的策略解析其 surface(bash、read/write/edit/…、横切的path门、external_directory、web_search、tool、skill)。deny直接拦、ask走审批(人工或 AI)、allow放行。 bash 的命令行用 tree-sitter(真 AST)解析,其中每条命令——包括$(...)、反引号、子 shell、sh|bash|… -c '…'脚本(递归重析)里的嵌套命令—— 分别裁决:匹配bash模式与path门(对拼接命令与每个 token 各匹配一次, 所以"path": { "*.env": "deny" }能拦住任何位置的cat .env),并结构化检测 项目外路径与提权(env/nice/xargs包装、bash -c '…'脚本都能识破)。 tree-sitter 语法加载失败时,用整串 token 扫描启发式兜底(局限见 SECURITY.md)。 - OS 沙箱(
@anthropic-ai/sandbox-runtime):bash 的真正执行约束。模式沙箱 开启时,项目内 bash 被sandbox-exec(macOS)/bubblewrap(Linux)包装, 写被限制在allowWrite(项目 +/tmp/pi+ 会话 scratch 目录)内,denyRead的密钥路径读起来像不存在——无论命令写了什么。alt+s的状态 决定哪些模式跑在沙箱里(off= 不沙箱);strict额外在策略层拒绝一切越界访问。
所以:策略说 allow 的 bash 仍在沙箱内运行;ask 弹窗后沙箱内执行。
会脱离沙箱的只有提权、写越界,以及沙箱 off/不可用时的命令——都需你亲自授权;
只读越界仍在沙箱内、由策略/分类器把关;deny 永不执行。
注意:只有模型的
bash工具被 OS 沙箱约束。文件工具(read/edit/write/…) 由策略引擎与路径检查管,不走 bubblewrap——Plan 的 Markdown-only 规则与保护路径 兜底因此都在工具层执行。沙箱内的读保持宽松(仅denyRead密钥在内核层被拦), 沙箱off时不约束任何东西。完整威胁模型见 SECURITY.md。
沙箱感知。 沙箱模式激活时,每回合向系统提示注入一段事实性的
## Sandbox & permissions:合并后 profile 的可写路径、拒绝读、网络 allowlist
(含项目 overlay),以及询问流程如何工作。模型因此会选真正可行的路径与域名
——项目内安装而不是 ~/.npm,allowlist 内的主机而不是死链——并且知道越界命令
尽管发(你会被问到)。strict 态会额外注明"项目外读写被直接拦截"。沙箱降级时
该段会说明并指向确认弹窗。用 "injectSandboxInfo": false 让某模式不注入;
沙箱 off 的模式只注入一段简短的 scratch 目录说明。
工具隐藏。 模式的 hideTools 列表会在模型推理之前把这些工具从可用集
移除(走 active-tools allowlist),模型根本不会尝试。它只会移除:你自己关掉的
工具(settings.json 的 defaultTools、--exclude-tools)保持关闭,切模式时
精确恢复上一个模式隐藏的部分。列表按字面生效,包括本扩展的 show_plan——
不做规划的配置可以去掉它;带 "@plan" 提示的模式若隐藏它,加载时会告警
(那个提示让模型调用它)。
技能与扩展工具。 技能在 /skill:<name> 处按名门控(skill surface);
任何非内置/扩展工具(包括宿主添加的 MCP-as-tool)按工具名门控(tool surface)。
在除 accept 外的模式(auto 下该决定交给分类器),宿主新加的、还没有规则的
工具/技能首次使用时弹窗——Allow once / Allow this session / Allow forever / Deny。"Allow forever" 把
<mode>.permission.<tool|skill>.<name>: "allow" 持久化到全局配置,之后不再问。
Accept 对所有工具/技能免问。(把某模式的 tool/skill 设为 "allow" 可退出
首次询问,设为 "deny" 可整体禁止。)
保护路径。 除非模式设置 bypassProtectedPaths(只有 accept 这么做),
对保护路径的 edit/write 一律硬拦——按路径段匹配(不是宽松子串),同时匹配
字面目标和其符号链接解析后的规范路径(指向 .git/ 或 dotfile 的链接无法
走私写入)。集合与沙箱运行时的强制拒绝清单一致:.git/、node_modules/、
.vscode/、.idea/、.env/.env.*、.claude/{commands,agents}/、常见 dotfile
(.bashrc、.zshrc、.profile、.gitconfig、.npmrc…)。这在 edits 模式
最重要——文件工具不受 OS 沙箱约束。
配置
模式是数据,按此顺序叠加:
- 出厂默认 ——
permission-mode.defaults.json,随扩展发布(五个内置模式)。 与你编辑的格式相同;复制一份即可自定义,或用/permission init在下面的全局 路径生成一份副本。 ~/.pi/agent/permission-mode/permission-mode.json(全局,完全权威): 重定义内置模式、新增模式、设置defaultMode/cycleOrder/sandbox/network/classifier/statusFormat。(路径稳定,与pi install的安装 位置无关。)<项目>/.pi/permission-mode.json(项目级,只能收紧):只能让已有模式 更严格。其权限策略作为最严格 overlay 叠加(只能更多地ask/deny, 绝不放宽——无论用什么模式写),沙箱按更严格方向 intersect/union,network同理。项目配置不能新增模式、改默认值或放宽任何东西。打开不信任的仓库 永远不会削弱你的保护。
形状
{
"$schema": "https://raw.githubusercontent.com/qingshanblue/qings-pi-modes/main/schemas/permission-mode.schema.json",
"defaultMode": "ask",
"cycleOrder": ["plan", "ask", "auto", "accept"],
"outsideWriteRoots": ["~/notes"], // 文件工具可直接写入的项目外目录(仅全局;受保护路径仍拦)
"sandbox": { // 共享 OS 沙箱 profile(一处定义;仅全局)
"allowWrite": [".", "/tmp/pi"], // alt+s 的三态围绕这份 profile 切换启停
"denyRead": ["~/.ssh", "~/.aws", "~/.gnupg"],
"allowRead": [], // 在 denyRead 内重新可读(见下方"Strict home")
"denyWrite": []
},
"network": { // 网络边界 —— 独立轴(alt+n)
"allowedDomains": ["github.com", "*.github.com"],
"deniedDomains": [],
"askOnBlockedHost": true // allowlist 外的主机实时询问(false = 静默拒绝)
},
"statusFormat": "compact", // footer 详细度:"compact"(默认)或 "verbose"
"classifier": { // AI 安全分类器(供带 "classifier": true 的模式使用)
"model": "current", // "current" = 当前会话模型;否则 "provider/model"
"timeoutMs": 15000,
"failClosed": true // 出错/超时 → 拦截(false = 降级为人工弹窗)
},
"modes": {
"ask": { // 这里是示意 —— /permission init 可拿到真实默认值
"label": "Ask",
"color": "muted", // muted | mdLink | accent | error | warning
"systemPrompt": "@plan", // 可选;"@plan" = 带日期的 Plan 模式提示
"injectSandboxInfo": true, // 把本模式的沙箱边界注入系统提示(默认 true)
"classifier": false, // true = 本模式的 ask 交给 AI 分类器
"sandboxPin": "on", // 可选:本模式激活期间强制沙箱态(off|on|strict)
"permission": {
"path": { "*": "allow", "*.env": "deny" }, // 横切门(deny 覆盖逐工具 allow)
"external_directory": "ask", // 项目外边界
"read": "allow", "grep": "allow", "find": "allow", "ls": "allow",
"write": "ask", "edit": "ask",
"bash": { "*": "ask", "sudo*": "deny" }, // 按命令(名 + 参数)匹配
"web_search": "ask",
"tool": "allow", // 任何非内置/扩展工具,按名
"skill": "allow" // 技能,按名
},
"hideTools": [] // 本模式下对模型隐藏的工具
}
}
}
outsideWriteRoots 是静态的人工授权:文件工具(write/edit)写入这些目录时不询问、不交 AI;
它不改变 bash 的沙箱边界(bash 仍受 sandbox.allowWrite 约束),也不能被项目配置设置。
动作:allow(放行)、ask(询问)、deny(阻止)。surface 的值可以是单个
动作,也可以是 { "<glob>": <action> } 映射:* 匹配包括 / 在内的任意字符、
? 匹配单字符、~/$HOME 会展开、后匹配的模式获胜(把 "*" 放在最前作默认);
跨 path / external_directory / 各 surface 层时最严格者获胜。
保持全局配置不过期
/permission init 会写入一份完整的出厂默认副本供你就地修改。副本里的每个值都会
覆盖出厂默认,这意味着之后默认值的变更(例如 2.3.0 把 allowWrite 从 /tmp
收紧到 /tmp/pi)不会到达仍带旧值的副本。两个机制兜底:
- 过期默认告警,每次会话启动。 全局文件里的每个值都会与当前默认、以及
自 2.0.0 以来发布过的每个旧默认(
defaults-history.json,带版本区间的明文 副本)比对:等于当前默认 → 无事;不同于当前默认但等于某个旧默认 → 按字段 报告(含两侧值与所属版本区间);不等于任何默认 → 是你的自定义,保持静默。 自定义模式永不触发。 - 升级后的一次性提示。 扩展版本变化且两个版本间默认值有变时,持有全局配置
的用户会被告知哪些字段变了——重定制的文件即使没有任何值命中旧默认也会被
提醒去比对。最后见过的版本记录在
~/.pi/agent/permission-mode/state.json。
想保留过期值,设 "acknowledgeDefaults": "<版本>"(你比对时所针对的版本;
/permission init 会在 $comment 里记录副本来源)。在出厂默认再次变化前告警静默。
更简单的做法是删掉你无意修改的值:文件是 overlay,省略的项跟随默认。
自定义模式
在全局配置的 modes 下新增模式,并(可选)加入 cycleOrder。示例——一个
"review" 模式:web 搜索关闭、edit/write 工具整体隐藏:
{
"cycleOrder": ["plan", "ask", "edits", "auto", "accept", "review"],
"modes": {
"review": {
"label": "Review", "color": "mdLink",
"sandboxPin": "on",
"permission": { "read": "allow", "bash": "ask", "web_search": "deny", "write": "deny", "edit": "deny" },
"hideTools": ["edit", "write"]
}
}
}
Linux glob 限制(仅沙箱文件系统)。 沙箱运行时在 Linux 上会丢弃
allowWrite/denyRead/denyWrite里的 glob 模式——这些列表请用字面路径 (macOS 支持 glob)。这只影响沙箱列表;permission的策略 glob 由本扩展 匹配,不受影响。
Strict home(可选)。 项目外的读是"deny 列表"式而非"allow 列表"式:沙箱只拦
denyRead(默认是密钥文件/目录,见出厂默认),~下其余内容都可读以便工具链 工作。allowRead可以反转某个模式:先 deny 整个 home,再只放行工具链需要的 路径(deny-then-allow;carve-out 内更具体的denyRead或会话 block 仍然获胜)。 粘贴到模式的sandbox并按需精简:"denyRead": ["~"], "allowRead": [".", "/tmp/pi", "~/.gitconfig", "~/.cache", "~/.config", "~/.local", "~/.npm", "~/.nvm", "~/.cargo", "~/.rustup", "~/go", "~/.pyenv", "~/.gem"]
.必须保留:否则项目本身在沙箱内会读成空。~/.gitconfig也要保留,否则 沙箱内每次git commit都会报 "Author identity unknown"(沙箱把该文件报告为 不存在而非拒绝,其它信息不会告诉你原因)。扩展会自动重新放行自己的运行时目录 (沙箱的 seccomp 助手位于~/.pi/agent/...下);运行 pi 的解释器也无需列出, 需要列出的只是 agent 命令会执行的东西(~/.nvm、~/.cargo、项目外的 venv)。~下未列出的路径在 bash 内读作不存在(Linux 在其上挂一个空目录;无报错、 无弹窗),漏掉的工具所需路径会静默失败;/sandbox显示清单,awareness 段落 也会告诉模型什么被遮蔽。~/.config与~/.local是大多数 CLI 存放配置与令牌 的地方(gh等);只有当 agent 完全不需要那些工具时才省略。项目配置可以 移除 carve-out,不能新增。 注意默认denyRead里的~/.pi/agent/auth.json意味着从沙箱内启动的嵌套pi无法鉴权;如果你那样嵌套 pi 会话,请把它移出 denyRead。
各平台的临时目录。 运行时总是允许写自己的
/tmp/claude,与allowWrite无关;macOS 上/tmp是/private/tmp,两种写法都处理。macOS 的用户级临时目录/var/folders/…不可写(runtime 0.0.26 时代可以);沙箱内 bash 的TMPDIR指向会话 scratch 目录。Windows 无 OS 沙箱,那里的allowWrite只参与提示边界。 硬编码/tmp且无视TMPDIR的工具在沙箱内会静默失败(无弹窗——内核直接拒); 需要就把/tmp加回该模式的allowWrite,代价是与宿主机共享/tmp(并设置acknowledgeDefaults让过期默认告警安静,见上文)。
禁用
- 临时:
--no-extensions启动 pi(禁用全部扩展),或--no-sandbox(保留模式但去掉 OS 沙箱)。 - 永久:
pi remove git:github.com/qingshanblue/qings-pi-modes(与安装方式一致) —— 或从 pi 设置里移除对应条目。
与上游的差异
本 fork 相对 wynainfo/pi-permission-modes 的变更(完整清单见 CHANGELOG.md):
- 模式重划为 CC 式五档:
plan / ask / edits / auto / accept(原 default/plan/build/ yolo 重映射:default→ask、build→edits、yolo→accept,plan 保留,新增 AI 档 auto;edits= accept-edits,accept不再强制关沙箱); - 新增
auto的 AI 分类器(safe-command.ts只读白名单 + 用户请求上下文 + fail-closed + 连续拒绝转人工); - 沙箱解耦为独立三态(
off/on/strict,alt+s),profile 上提为顶层共享块; 新增顶层network块与statusFormat; - 分类器可见用户请求(修复无上下文导致的误拦);
- 切换不再弹通知,footer 状态条为唯一事实来源(token 化 + 三态配色);
plan的只读改走策略层(bash 变更弹窗 + 白名单),不再依赖沙箱 ro。
开发
git clone https://github.com/qingshanblue/qings-pi-modes.git
cd qings-pi-modes
npm install
npm test # node --test ./src/*.test.ts
npm run typecheck # tsc --noEmit
设计决策与实施记录见 plan/;本地加载调试:
pi install /path/to/qings-pi-modes # 本地安装(改代码即时生效)
pi -e ./src/index.ts # 单次加载
License
MIT —— 见 LICENSE。基于 wynainfo/pi-permission-modes(MIT)。