dsh-whitelist-auto-approval

securitytested: passnpmMIT

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

下载量 / Downloads
1,397
依赖体积 / node_modules
0.2 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

Install

dsh plugin --profile web add dsh-whitelist-auto-approval

Sandbox test results

StageResult
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

Links

GitHub · Homepage · npm

Same category