DeepSeek Harness Hub
← 返回列表

azazo1/dsh-write-protect

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
✓ 可直接安装

防止模型改写工作区里的指定路径. 同时支持放开某些外部目录的写入而不必放开沙箱.

自动检查通过:npm 包已发布且 engines 声明满足基线(声明 Node >=20.11);该结论来自程序自动检查,未经人工实机验证。 · 最近上游提交 2026/9/20 · 已提供中文文档
综合分
36.2
GitHub 分
36.2
用户评分
★ Stars
3
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add dsh-write-protect
npm 包 dsh-write-protect 已校验归属本仓库,走 npm 安装最省事
数据截至 2026/9/20(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查✓ 自动检查通过

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

npm 包dsh-write-protect @ 0.1.1
Node 引擎要求 >=20.11 · 基线 Node 22.19 满足
dsh CLI 依赖未声明 dsh 版本约束
入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/20 06:31:17

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-client-ui-settings@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-client-ui-renderer@deepseek-ai/dsh-fs@deepseek-ai/dsh-fs-local@deepseek-ai/dsh-fs-sandbox@deepseek-ai/dsh-sandbox@deepseek-ai/dsh-sandbox-local@deepseek-ai/dsh-sandbox-policy@deepseek-ai/dsh-session-projection@deepseek-ai/dsh-settings
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-write-protect

给 DSH 沙箱补上工作区里某一段路径的只读保护, 典型用途是不让模型改 .git. 也可以在 workspace-write 下声明工作区外的额外可写根, 让 bash 与 write / edit 写到相邻目录, 而不必切到 danger-full-access.

write / edit 工具在所有平台都会挡住保护路径, 并放行额外可写根. bash 等命令在 Linux / macOS 上同样生效; Windows 上 bash / pwsh 既挡不住 .git, 也拿不到额外可写根. 读取不受影响.

官方沙箱只有 "整个工作区可写" 和 "全只读" 两档, 管不到工作区内部的某一段, 也不能把工作区外的个别目录并进 allow-list; Codex 一类实现默认会保护 .git, 本插件补这一块.

安装

dsh plugin --profile web add azazo1/dsh-write-protect

固定版本:

dsh plugin --profile web add azazo1/dsh-write-protect#v0.1.2

GitHub Release 同时挂不带版本号的预构建包, 安装时跳过 allowBuilds:

dsh plugin --profile web add https://github.com/azazo1/dsh-write-protect/releases/latest/download/dsh-write-protect.tgz

本地检出用一键脚本 (scripts/install.sh, Linux / macOS, 可传 profile 名, 默认 web):

./scripts/install.sh              # 装进 web profile
./scripts/install.sh headless     # 装进指定 profile
./scripts/install.sh --dsh-home ~/.dsh --profile web

脚本是检出内工具, 不随 npm 包发布. 它先把当前检出拷进 /plugins/dsh-write-protect 再 dsh plugin add 注册. 必须先复制: 就地 link 的检出会被 Node 解析回真实路径, 从检出目录向上走不到宿主自己的 @deepseek-ai/, boot 会报 Cannot find package '@deepseek-ai/dsh-sandbox-local'. 从 npm / GitHub 安装没有这个问题 (pnpm 会把包物化在 profile 里). 复制会跳过 .git / node_modules / .tmp / dist / .agent; profile 模板本身关掉了 peer 自动安装 (autoInstallPeers: false), 所以宿主提供的 @deepseek-ai/ 不会被装进 profile 遮蔽宿主. 装完重启应用生效.

安装后会接管沙箱策略和 write / edit 围栏, Linux / macOS 上还会接管命令沙箱. 改配置即时生效, 不用重启 dsh web.

引擎版本线跟随 @deepseek-ai/dsh- 的 0.1.6-alpha.1 (peerDependencies 同号). 官方 SandboxProvider.confine() 自 0.1.6-alpha.1 起改为异步 (Promise 加 signal 参数, 0.1.5-rc.1 仍是同步签名), 本插件的覆写同样异步; 还在 0.1.5 及更早引擎上的部署请继续用 v0.1.1.

展开是有界的, 这一点对"装完插件整个 dsh web 卡住"很关键. policy.resolve() 是同步契约, 而默认保护条目 .git 不带 /, 属于非锚定通配 —— 要找到任意层级的匹配就必须遍历整个工作区. 工作区一大 (把家目录当工作区是一类常见情况), 一次同步遍历就是几十秒到几分钟, 期间 Host 事件循环被占死, 而 resolve() 在 systemPrompt 组装、bash / 终端 spawn、fs 写围栏、设置页预览上都会走到, 于是整个 web 连首页都打不开. 现在的取舍是: 同步遍历最多 500 个队列项 / 50ms, 被截断时先把已找到的路径用上 (广度优先, 浅层优先, 工作区根上的 .git 通常头两项就命中) 并告警, 深层匹配交给后台分片遍历 (最多 20000 项 / 10s) 补齐后自动并入, 补齐结果不会被更差的同步结果覆盖. 家目录级工作区仍可能漏掉最深处的匹配, 需要无条件覆盖时请改用锚定条目 (/.git), 或按仓库分别声明路径.

配置

保护路径的默认值统一定义在 src/constants.ts 的 DEFAULT_READ_ONLY_PATHS, 额外可写根默认空列表 (DEFAULT_WRITABLE_PATHS), macOS broker 加固默认开启 (DEFAULT_HARDEN_BROKER); patch 的 policy 行与设置页部署 base 都由它们兜底. 需要部署级覆盖时在 patch 行显式给出数组或开关:

- id: dsh-write-protect-policy
name: dsh-write-protect
config:
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
workspaceRoot: !!js process.cwd()
部署级覆盖示例.
readOnlyPaths: ['.git', '//etc/pki']
writablePaths: ['../shared-scratch', '//tmp/dsh-extra']
hardenBroker: false

readOnlyPaths 的每一项是一行 gitignore 语义的模式, 数组逐行合并为生效文本:

- 不含 / 的条目 (如 .git, vendor) 在工作区内任意层级匹配, 覆盖嵌套仓库等场景.
- 以 / 开头或含中间 / 的条目锚定到工作区根 (如 /.git, dist/a.txt); 字面条目即使尚不存在也保留保护, 例如 git init 之前的 /.git.
- 以 // 开头的条目是文件系统绝对路径(如 //etc/pki),这是本插件额外支持的写法,gitignore 没有这种形态。
- 尾部 / 表示只匹配目录(如 build/)。
- 通配: 匹配单段内任意字符,? 匹配单字符,[...] 字符类(含 [:alpha:] 等 POSIX 类), 独立成段时递归(如 a//b);\ 转义下一字符(\#、\!,尾部空格用 \  保留)。
- ! 开头剔除匹配项,按 gitignore 的 last-match-wins 顺序解释;受保护目录内部无法通过取反重新放行后代。
- write / edit 按模式逐路径判定,与路径是否存在无关;bash / pwsh 侧的保护路径是枚举出来的,只覆盖展开时刻已存在的路径,且非锚定通配(如 .git)在超大工作区上受展开预算限制(同步遍历先覆盖浅层,后台补全再往里补,仍有未覆盖到的匹配时 Host 日志会告警)。解析时会解开符号链接并去重。
- 置为空列表 [] 即停用保护(插件仍在,只是不再多挡任何路径)。

writablePaths 的每一项是一行字面路径,不是 gitignore glob:

- 行首 ~ 或 ~/... 展开为当前用户家目录;~other 不支持。
- $NAME 与 ${NAME} 展开为环境变量;未设置或空值的变量整行丢弃并告警。\$ 保留字面 $。
- 宿主绝对路径(/tmp/extra)或 // 前缀(//tmp/extra)按文件系统解析。
- 其余相对当前会话工作区,含 ..(如 ../sibling-project)。
- 工作区内的路径本来就可写,展开时忽略并告警;文件系统根(/ 或盘符根)拒绝,避免把只读宿主根整棵翻成可写。
- 不支持通配与 ! 取反。不存在的路径仍保留词法形态:write / edit 与 Seatbelt 可按前缀放行,bwrap / Landlock 在叠加时跳过并告警。
- 只在 workspace-write 下并进 allow-list,不打穿 read-only。保护路径优先:额外根内部仍可被保护。
- 置为空列表即不额外放行。

hardenBroker 是 macOS broker 逃逸加固的部署 base,布尔值,缺省 true:

- 开启时在 Seatbelt profile 末尾追加 broker 拒绝形式(见“保护范围”)。
- 关掉后命令按官方 profile 运行,只影响 macOS,只影响这一个加固;保护路径与额外可写根照常。
- 用户在设置页拨动开关后该值不再生效。

设置页

Web Settings 侧边栏的“写入保护”页面有三块内容:保护路径(gitignore 语义)、额外可写根(字面路径)和 macOS broker 加固开关。保存后实时生效并持久化:

保护路径
.git
secrets/.pem
!secrets/example.pem

额外可写根
../shared-scratch
~/scratch
$HOME/scratch
/tmp/dsh-extra

- 保护路径:# 开头是注释,空行忽略;! 排除,按最后匹配生效;不能在仍受保护的目录内部重新放行后代。通配与锚定语义同“配置”一节。Windows 上的绝对条目写作 //C:/Users/me/secret:gitignore 语义里 \ 是转义符,/ 才是分隔符。
- 额外可写根:每行一条字面路径,不要通配。~ / ~/... 为家目录,$NAME / ${NAME} 为环境变量;绝对路径按文件系统解析,相对路径(含 ..)相对当前会话工作区。Windows 上 \ 是分隔符而不是转义符,C:\Users\me\caches 与 ~\caches 都按字面解析;盘符相对路径(C:caches)的落点取决于进程当前目录,会被拒绝并出现在“未生效”里。
- 两份文本都按当前会话的工作区根解析,每个会话各自生效。开关是全局的,与工作区无关。
- 预览按钮把当前草稿交给 Host 展开,不必先保存:列出生效的保护路径与额外可写根,以及被忽略或拒绝的行。展开使用当前选中会话的 cwd;没有选中会话时回退到部署工作区根(通常是 dsh web 的启动路径)。预览走异步完整展开(不受同步的短预算影响),在家目录级工作区上要等几秒,期间不会阻塞 dsh web;到异步预算上限时返回已展开的部分并在“未生效”里说明。

mode 与 workspaceRoot 是官方 policy 行字段的复述(patch 对整行配置做替换,必须带上),取值语义与 base bundle 一致。

保护范围

保护路径与额外可写根会同时作用在下面几个入口,解析结果是同一份:

| 入口 | 哪些系统 | 效果 |
|---|---|---|
| write / edit 工具 | 全平台 | 按保护模式逐路径判定,命中即拒绝(与展开预算无关);workspace-write 下额外根放行 |
| bash 等命令 | Linux, macOS | 内核级只读 / 额外可写;Windows 做不到,见下方限制 |
| 提示词 | 全平台 | 先告诉模型哪些不能写,哪些额外根可写 |
| macOS broker 加固 | macOS | 堵住 open 经 launchd 把命令挪到沙箱外执行 |

两类入口的判定方式不同,这是有意的:write / edit 拿得到目标路径,因此直接按 gitignore 模式判定 —— 深层嵌套、尚未存在、枚举没覆盖到的匹配一样挡得住,每条写入只做几次正则;bash 的沙箱(mount / profile)只能吃具体路径,所以那一侧才需要枚举展开,也才有预算与补齐这回事。

主场景是 workspace-write。read-only 下官方已挡住全部文件写入,额外可写根不打穿;但官方 profile 的 (allow default) 在两种模式下都一样,所以 broker 加固不区分模式。

macOS broker 逃逸加固
官方 macOS profile 是 (version 1) (allow default) (deny file-write) ...,mach-lookup 与 process-exec 全开。而经 launchd 代理启动的进程不继承 Seatbelt profile,于是沙箱内一条 open x.app 就能让启动的进程在沙箱外任意读写,deny file-write 被整条绕开 —— read-only 同样会被打穿。本插件在 profile 末尾追加:

(deny mach-lookup (global-name-prefix "com.apple.coreservices"))
(deny appleevent-send)
(deny mach-priv-task-port)

SBPL 按 last-match-wins 解释,追加在末尾才能盖过 (allow default)。com.apple.coreservices 是 LaunchServices 的服务名段,open / NSWorkspace 靠它把请求交给 launchd;名称过滤器按 reverse-DNS 分段匹配,所以只能整段拒绝,收窄到子服务无效。appleevent-send 关掉让别的 app 代劳那条路,mach-priv-task-port 关掉注入已运行进程的 task port。

加固只做收紧,不放宽任何位置;常规命令(node、git、pnpm、python、curl、tar、rsync 等)不受影响。

设置页的 "macOS broker 逃逸加固" 开关与 patch 的 hardenBroker 控制这一个加固是否生效,缺省开启。关掉后 provider 原样返回官方 argv,适合确实需要从沙箱内驱动宿主 GUI 的场景;关掉即恢复可以被 open 打穿的状态。保护路径与额外可写根的叠加不受这个开关影响。

patch 配置和设置页文本走同一套解析。

边界与已知限制

- Windows 上 bash 挡不住,也放不宽:write / edit 能挡保护路径、能放行额外根;bash / pwsh 两者都不行。Windows 沙箱只能把整个工作区设成可写或不可写。
- broker 加固只在 macOS 生效:官方 macOS profile 的 (allow default) 让 open 能把命令交给 launchd 在沙箱外跑,本插件追加的拒绝形式堵住这条路。Linux 的 bwrap 用 mount namespace,没有 launchd 那类代理通道,但它的网络命名空间未隔离,沙箱内仍可连宿主守护进程(Docker socket、ssh-agent 一类)让外面代劳,这类问题本插件不处理。
- Linux 没有 bwrap,落到 Landlock 时:没法单独保护子路径,命令按官方沙箱跑并告警一次;额外可写根可以加 --rw。write / edit 两者都生效。
- 完全放开沙箱时(danger-full-access):bash 不进沙箱,挡不住;write / edit 仍然挡。
- Linux bwrap 要求路径真实存在:通配扫出来的保护路径如果当时还不在磁盘上,会跳过这条只读挂载并告警。需要无条件保护的工作区根路径请用字面条目(如 /.git);字面条目即使还不存在,write / edit 也会拒绝。
- 枚举只覆盖展开当时已经存在的路径(只影响命令侧):bash / pwsh 的保护路径清单是枚举出来的,新建路径最迟在下一次展开时纳入;部分(被预算截断)结果缓存 5 秒后重算,完整结果与已放弃补全的根缓存 60 秒;已经要保护的目录不会再往里扫,里面的匹配项不再单独列出;被 ! 放行的目录还会继续找。目录符号链接不跟随,避免扫到工作区外。write / edit 不受这条限制:它直接按模式判定,新建的 .git 立刻就被挡。
- 超大工作区上只有命令侧是有界覆盖:家目录级工作区里更深、更靠后(广度优先队列更晚)的匹配可能落在同步预算与后台补全预算之外,此时 Host 日志会告警。write / edit 仍然是完备的(按模式逐路径判定);要让命令也无条件挡住,请用锚定条目(/.git)或按仓库分别声明路径,别依赖 /.git 一类的全局通配。
- 尾部 / 按那个目录本身保护:和保护其下全部后代等价,同时避免枚举全部后代,代价是该目录自己也写不了。
- 指向保护目录内部的符号链接会被拒绝,指向外部的不受影响。

本地开发

just install    # 安装依赖
just typecheck  # TypeScript 类型检查
just build      # 构建 lib/
just test       # 测试套件 (Seatbelt e2e 仅在 macOS 上运行)
just verify     # 以上全流程 + 打包预览

改完源码想在真实 DSH 里手工验证时,先 just build,再用 ./scripts/install.sh 把当前检出装进 profile(脚本装的是 lib/,源码比产物新时会提醒)。

源码分三块,边界是"有没有文件系统依赖":

| 文件 | 职责 | 依赖 |
|---|---|---|
| src/gitignore.ts | gitignore 语义的解析、编译与逐路径匹配(含 PatternSet.match),write / edit 围栏的判定核心 | 纯字符串/正则,零运行时依赖 |
| src/patterns.ts | 把模式枚举成具体路径(有界同步 + 后台分片补全),供 bash 沙箱与提示词使用 | node:fs、canonicalPath |
| src/fs.ts / src/policy.ts / src/provider.ts | 三个挂载点:write/edit 围栏、沙箱 policy、进程沙箱 argv 叠加 | DSH 引擎 |

src/path-expand.ts 负责额外可写根的字面路径展开(~ / 环境变量 / 平台差异)。
测试覆盖: 纯匹配器语义 (锚定, *, 字符类, 取反, 前缀围栏, 目录标记, 大小写, 工作区外不match; 不需要任何临时目录), 路径解析语义 (相对锚定, 解开符号链接, 去重, 通配枚举与取反, 额外可写字面路径), 展开预算与异步补全 (同步截断时先给浅层结果, 后台补全后深层匹配出现且不被更差的同步结果覆盖), write / edit 按模式判定 (启动后才出现的深层路径、尾部 / 与同名文件、工作区边界), bwrap / Seatbelt / Landlock 的命令行叠加, write / edit 工具的拒绝与额外根放行矩阵, settings 通道的 base 与用户覆盖分层, client bundle 的 loader 注册, macOS 上真实 sandbox-exec 的内核级端到端 (包括 open broker 逃逸的对照组与加固后的拦截验证), 以及 Linux 上真实 bwrap 的内核级端到端 (保护路径写入 EROFS, 读取照常, 额外可写根可写; 本机 bwrap 不可用时整组跳过).

License

MIT

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群