dsh-whitelist-auto-approval
A per-command sandbox allow-list for the bash tool: a rule names a CLI and the writable roots it gets, so allow-listed programs such as lark-cli and gh run inside the kernel sandbox with the workspace plus those roots appended. Unlike plugins that answer the approval for you or that widen the writab
Install
dsh plugin --profile web add dsh-whitelist-auto-approval
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-whitelist-auto-approval
简体中文 | [English](./README.en.md)
给 DSH 的 bash 沙箱加一份按命令生效的白名单:让 lark-cli、gh 这类必须写用户目录的命令不再触发 danger-full-access 授权弹窗,同时不把整个会话切换成完全权限。
命令只写工作区 → 不需要本插件(默认就能写) 命令要写 ~/.xxx、~/Library → 加入白名单,仍然受内核沙箱约束 非白名单命令 → 行为完全不变,该弹窗照旧弹
和同类插件的区别
DSH 上已有一批解决"工作区外写入"的插件。按放行后命令在哪执行,大致分四类:
做法 · 放行后命令在哪执行 · 代表
按命令追加可写根(本插件) · 内核沙箱内,工作区保留,$HOME 作为第二根追加 · 本插件
扩展沙箱可写目录(配置级) · 内核沙箱内,配置的目录对所有命令生效 · dsh-write-protect、@morlay/dsh-sandbox-local
命令白名单,allow 含升级授权 · 沙箱外 · dsh-sandbox-allowlist
审批代答 · 沙箱外,返回 allowed-once · dsh-dhe-allow、dsh-auto-approval-plugin
差距不只在"少点几次弹窗",而在放行时失去多少隔离:后三类要么让命令直接到沙箱外跑,要么把额外可写目录做成对所有命令生效的全局配置;本插件的可写根只对命中的那条命令生效,且匹配的是可执行文件而不是命令字符串。
它只解决这一件事。需要 deny 规则、网络策略或按目录授权时,用上表里对应的方案更合适。
它解决什么问题
DSH 的 workspace-write 沙箱只允许写三类位置:会话工作区、/tmp、系统临时目录(os.tmpdir())。
而 lark-cli 刷新 token 时要写 ~/Library/Application Support/lark-cli/,平时还要写 ~/.lark-cli/(cache / locks / logs)。这些都在工作区之外,于是:
1. 命令被内核拒绝;
2. 模型按工具描述带 sandbox_permissions: danger-full-access 重试;
3. 你每次都要点一次授权;
4. 如果干脆把会话切成"完全权限",那整个会话都失去文件隔离。
本插件在 macOS / Linux 上把第 1 步变成"这次调用的可写根 = 会话工作区 + 该规则声明的目录"
(默认 $HOME,用 roots 可以收窄到 app 私有目录):token 能写,命令自己的下载/导出也能
写回工作区,于是第 2、3 步不再发生。
Windows(或无法识别 runner 的 provider)退回旧行为:把可写根换成 $HOME。
工作原理
DSH 里每条 bash 命令都会经过 ctx.shell.resolve(request) 拿到 sandboxPolicy,再由 ctx.sandbox.confine() 翻译成 runner 的 argv/profile。插件在这两个位置各装饰一层(macOS / Linux):
tool-bash ──resolve(request)──▶ 原 resolve() // workdir/timeout/policy 原逻辑
├─ 匹配命令(见下)
├─ 命中 → 保留 workspaceRoot,追加 extraWritableRoots = ['$HOME']
└─ 未命中/异常 → 原样返回
└─ ctx.sandbox.confine() ──▶ 原 provider 生成 argv
└─ overlay:在 profile/参数末尾追加可写根
overlay 按 runner 追加,不重建 profile:
- Seatbelt(macOS):在
-p的 profile 末尾追加(allow file-write* (subpath "$HOME"));SBPL 后匹配生效。 - bwrap(Linux):在
--前插--bind $HOME $HOME。 - Landlock(Linux):在
--前插--rw $HOME。 - 识别不出 runner、缺
-p/--、路径不存在、或该根已经在 profile 里时,原样返回并告警一次。
几点设计上的取舍:
- 不替换任何官方 row,只装饰
ctx.shell.resolve()和ctx.sandbox.confine()。overlay 失败时最坏是"这次没加上根",bash 和沙箱本身照常工作。 - 只放宽"写哪里",不改审批策略、不改文件系统工具、不改会话模式。
- 失败安全:任何解析不确定、配置非法、正则写错、runner 不认识,都退回原沙箱 + 原审批——宁可多弹窗,不会少弹窗。
- Windows 不适用 overlay:退回旧行为(
workspaceRoot换成$HOME),因为 ACL 沙箱没有可追加的 argv/profile。
匹配规则
模型给的是一整条 shell 字符串,不是 argv。插件只回答一个问题:这条命令的写足迹,是否落在被信任程序的写足迹之内。判据只有一条:
放行时,整条命令能写的位置 ⊆ 匹配到的那个程序自己能写的位置。
这条判据不关心命令怎么拼写,只关心它还能不能写到别处。所以插件接受"一条简单管道":
- 第一段必须是放行的白名单程序:
argv[0]经PATH解析、realpath校验;落在会话工作区 //tmp/ 系统临时目录里的可执行文件一律拒绝(这是模型唯一能靠"往仓库里丢个同名脚本"劫持命名的路径)。 - 后面的管道段只允许写中立过滤器:
tail、head、wc,且参数只能是纯数字或固定开关(-12、-n 12、--lines=12、wc -l)。不认路径操作数,不认-f,也不认tee/sh/sed -i/awk/sort -o这类会写文件或再起程序的工具。过滤器同样要过PATH+realpath+ workspace/tmp 检查。 - 重定向只允许不打开任何路径的形态:
2>&1、1>&2、>&-、>/dev/null(>>同)。 - 引号内的字符不参与执行,保留原样(
--text "a; b"仍然可用);拒绝;、&、&&、$(...)、反引号、!、换行、heredoc、未闭合引号、8KB 以上的命令。 argv[0]必须是裸命令名,或配置里写死的绝对路径;./lark-cli、/tmp/x/lark-cli这类"模型自己指定的路径形式"不匹配。- 例外:一个开头的
cd <目录> &&(allowCdPrefix,默认开)。模型常按别的 agent 的习惯写成
cd /repo && lark-cli ...,虽然 bash 工具有 workdir 参数。cd 只是内建命令、只改工作目录,
和 workdir 是同一个请求,所以剥掉这段前缀、用 cd 目标作为工作目录继续匹配。
只认第一个 &&,剩下的部分仍按上面的管道规则解析。不想要这条例外就设 allowCdPrefix: false。
能被放行的写法
写法 · 结果
lark-cli docs +update ...(配 workdir) · ✅ 命中
cd /repo && lark-cli docs +update ... · ✅ 命中
cd /repo && lark-cli docs +update ... 2>&1 · ✅ 命中
同上,再接 \ · tail -12 · ✅ 命中
同上,改用 \ · head -n 20、\ · wc -l · ✅ 命中
同上,改成 > out.log 或 >\ · out.log · ❌ 会写文件
同上,改成 \ · tee out.log、\ · sh、\ · tail -f · ❌ 会写文件 / 再起程序
同上,改成 ; echo done、&& echo done · ❌ 第二条命令
同上,写 \ · tail $(echo 3) · ❌ 命令替换
2>&1 | tail -12 是模型最常写的形态,现在会命中。代价只有一个:模型看到的退出码变成 tail 的。tail 成功而 lark-cli 失败时,只能靠输出和 stderr 判断;这类"管道遮住退出码"的写法因此仍不推荐,只是不再因为写法弹窗。
不需要每个仓库都写 AGENTS.md
插件自己把这条约束告诉模型:它注册一个系统提示段(tool:bash:auto-approval,紧跟在 tool:bash 之后),内容由当前规则生成 —— 命中哪些命令、认哪几种写法、哪些写法会落回审批。
- 装到哪个 profile 就带到哪个 profile,不用在每个仓库的
AGENTS.md里重复; - 在设置页改规则后,下一轮对话的提示就跟着变,不需要重启;
- 不想让它占系统提示就设
promptHint: false。提示只是提示,真正的判断仍在匹配器:模型写错形态只会多弹一次窗,不会多放行。
AGENTS.md 仍然可以写,但它是"项目偏好",不再是本插件的必要配置。
窄根与「任意写法」(roots / shape)
形状检查和可写根的宽度是互相补偿的:根越宽,写法必须越严;根越窄,写法可以越松。两件事可以分开做。
第一步,只收窄根(不放松任何写法):
- name: lark-cli
command: lark-cli
roots: # 只让这两个目录可写,$HOME 其余部分不动
- ~/.lark-cli
- ~/Library/Application Support/lark-cli
shape: strict # 默认值,仍按上面的写法表
~/.zshrc、~/Library/LaunchAgents/、~/.ssh 都不再在可写范围里。这是本插件目前性价比最高的一次收紧,行为上没有任何放松。
第二步,需要复杂写法时再打开 shape: any:
- name: lark-cli command: lark-cli roots: [~/.lark-cli, ~/Library/Application Support/lark-cli] shape: any
这时插件不再解析语法,命中条件退化为「命令行里出现了这个程序名」(引号内的整体算一个词,注释和重定向目标不算)。
| tee、;、&&、for 循环、xargs 都能命中;写仍然被内核限制在列出的根里。
仍然拒绝的几种:
./lark-cli、/tmp/x/lark-cli这类模型自己指定的路径(影子攻击);bash -c 'lark-cli …':引号内是一个词,不会被当作程序名(宁可多弹一次窗);shape: any配$HOME(或$HOME的祖先目录):加载时直接丢弃该规则并告警 —— 「任意写法」只剩可写根这一道边界,把整个家目录交给它等于「任何命令都能写家目录」。
用 any 之前请确认这四条代价:
1. 目录必须列全:漏一个,命令会在执行中被 [sandbox: file access denied …] 拒,进而触发模型升级授权 —— 正是本插件要消灭的那条链路,频率取决于你列得准不准。
2. 名字匹配不再是 realpath 校验:shape: any 只看字符串,PATH 影子检查也不再适用,安全边界只剩内核的可写根。
3. app 私有目录也是持久化面:如果该 CLI 会加载目录里的配置或 hook,写进去等于下次执行代码。要按 app 判断,不能默认「窄 = 安全」。
4. overlay 不可用时额外根不生效:POSIX 上授权始终是「工作区 + 列出的根」,overlay 没装上时只有工作区可写,Host 日志会告警一次并在放行行标记 (root overlay inactive);不会退化成「只给第一个根」。
5. Windows:ACL 策略只能表达一个根,多根规则取第一个并告警。
建议顺序:先 roots + strict 跑一段时间,确认没有新的 file access denied,再把确实需要复杂写法的规则单独改成 `an
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::7,最后更新 2026-10-29