git-for-dsh
Allowlisted git execution for DeepSeek Harness: a model-facing git tool that runs outside the file sandbox, with a settings page where the user ticks which git subcommands are permitted and, per path, which reads and writes are allowed, prompted or refused 适合需要授权 AI 在沙箱外执行特定安全 Git 命令的开发者。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:theRMM714/git-for-dsh免责声明:这是个人测试用的项目,使用这个项目出现任何问题,作者不负任何责任,如果同意再进行使用。
一个 DeepSeek Harness 插件:让 AI 能在文件沙箱之外执行 git 命令,同时由用户自己勾选「允许哪些 git 子命令」。
插件由两半组成,装一次即可:
- Host 半(
src/index.js)注册模型可见的工具git_exec,并实现四道闸门:用户的允许清单、参数闸门、配置写入闸门,以及执行环境硬化。 - Client 半(
src/client.js)在「设置 → Git 工具」里渲染勾选页,用户在这里决定放行哪些子命令。
使用 git_exec
| 参数 | 必填 | 含义 |
|---|---|---|
argv | 是 | 程序名之后的 git 参数,第一个元素是子命令,例如 ["log", "--oneline", "-n", "20"] |
paths | 否 | 本次命令涉及的文件或目录,插件拼成 git … -- |
description | 是 | 一句话说明这条命令做什么,显示在界面上 |
workdir | 否 | 工作目录;默认会话工作区,相对路径按会话工作区解析 |
timeoutMs | 否 | 超时毫秒数;本地操作默认 120000,触及远端的操作默认 300000 |
justification | 否 | 需要审批时提供的一句话理由 |
返回 exitCode、signal、timedOut、operation、command、stdout、stderr。
文件路径放在 paths 里,不要塞进 argv:插件会拼成 git … -- ,因此空格、通配符、以 - 开头的文件名都不需要转义。
工具描述与系统提示段都从当前允许清单实时生成,模型任何时刻都能看到自己能用哪些操作。
3. 配置不能变成程序
只读操作会读取仓库配置,而配置里可以指定要执行的程序,这足以绕过整个允许清单:
git config --local core.fsmonitor "sh -c '…'" → git status 执行了它
git config --local diff.evil.command "sh -c '…'" + .gitattributes → git diff 执行了它
status 和 diff 都是默认放行的只读操作,因此「只放行看状态」实际上等于给了代码执行能力。三处封堵:
config只保留读取形式:最多一个操作数,且不接受--unset/--add/--replace-all/--edit/-f等写入或选文件旗标。git config user.name X会被拒绝。- 指定程序的配置键被钉死:
core.fsmonitor、core.gitProxy连同core.pager、core.hooksPath、credential.helper一起,通过环境变量注入(优先级高于任何配置文件)。ssh 也在其中,但方式不同:GIT_SSH_COMMAND被钉成-o BatchMode=yes -o StrictHostKeyChecking=accept-new,环境通道同样压过一切配置文件,所以仓库无法指定程序来当 ssh,而 SSH 传输仍然可用。 - diff 类子命令强制带
--no-ext-diff --no-textconv:diff..command是通配键,无法逐个钉死,因此在命令上关闭外部 diff 与 textconv。调用方传--ext-diff/--textconv会被拒绝,无法把它打开。
scripts/verify-driver-hardening.mjs 用真实 git 与一个恶意仓库验证这两条路径已关闭,并且带控制组(未加固时两个标记都必须出现),否则「没看到标记」可能只是没武装。
4. 仓库配置审计
前三项针对的是「键能被钉死」的情况。git 原生通配(filter..clean、url..insteadOf、alias.、merge..driver)没有对应的单个环境键可以覆盖,钉不死就只能拒绝。
所以每次调用前先跑一次 git config --local --includes --list --name-only -z,命中危险键就按配置的裁定处理:
| 裁定(设置页文案) | 行为 |
|---|---|
refuse-repo(一票拒绝,默认) | 该仓库里出现任何危险键,本工具在该仓库拒绝执行任何命令 |
refuse-affected(只拒受影响) | 只拒「会被该键影响」的子命令(例如 alias.* 劫持不了 git 内建命令,不影响 status) |
neutralize(尽量中和) | 已被钉死的键照常工作,只拒无法钉死的通配键 |
off(关闭审计) | 关闭审计:不检查仓库配置,因此也不为此多起一个 git 进程。危险键可以借此执行程序(core.fsmonitor、diff.*.command),只应在完全信任仓库时使用 |
这个命令有两条硬性要求:
- 必须带
--includes。git config --local --list看不到由[include]引入的键,而它们照样生效:一个被 include 进来的core.fsmonitor能执行却对--local --list不可见。不带--includes的审计会恰好漏掉「会执行的那些」。 --name-only必须与--list同用。只写--name-only时 git 以 exit 129(no action specified)失败;而「读不到配置」按设计不算拒绝,于是闸门会完全空转。该命令在代码里只有一处定义,并由scripts/verify-config-audit.mjs用真实 git 复查它确实能跑、能看到 include 进来的键、三种裁定都正确(off档不检查配置,因此不在此脚本范围内)。
审计也未必是「对抗 AI 的边界」—— 仓库配置就在工作区内,模型可以直接写它。它的价值在于不管配置怎么来的,运行前都会被检查:敌对仓库(clone 来、不是自己写的)尤其如此。
性能:审计结果按仓库配置状态缓存
一次 git_exec 会起两个 git 进程:子命令本身,以及执行前的仓库配置审计。在 WSL 上进程启动是毫秒级开销,所以审计结果会缓存 —— 但缓存键覆盖了判定所依赖的每一个输入:
- 运行目录;
- 危险键策略;
- 仓库自身配置的状态戳(
mtime + size,含config.worktree)。
配置一改,戳就变,缓存自动失效;缓存里存的是解析出的键名,而判定每次都按当前子命令重新计算,所以不存在「缓存住的结论」。
两个边界是写明的:
[include]引入的文件:它们的路径不额外问 git 就看不到(而问 git 正是缓存要省掉的调用),所以对被 include 的文件的修改由 30 秒 TTL 兜住,而不是立刻失效;- worktree(
.git是文件而非目录):没有可监视的配置,于是完全不缓存(宁可每次都跑,也不缓存一个无法验证的东西)。