bibibala
dsh-git-guard
Git-aware write guard plugin for DeepSeek Harness: blocks whole-file writes that would overwrite the user's uncommitted changes, and reports what happened to protected files at turn end
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 14, 2026
- Updated
- Aug 14, 2026
Introduction
dsh-git-guard
A git-aware write guard plugin for DeepSeek Harness. It stops the agent from silently destroying the user's uncommitted work: at each turn start it snapshots every file with uncommitted changes (staged, unstaged, or untracked) in the session workspace's repository and backs them up; a whole-file replacement tool call (the write tool by default) targeting such a file is denied at tools/pre-execute with corrective feedback; at turn end the protected files are re-verified and the outcome is reported as a plugin-source notice in the session log. The guard runs its own credential-scrubbed git subprocess — never the model's shell.
Install
The plugin is distributed as a bundle: an npm package with a dsh.bundle manifest that contributes one configuration layer.
dsh plugin --profile web add dsh-git-guard
For a git install (no npm package): dsh plugin --profile web add github:you/dsh-git-guard — the first add fails until you allow the package's build script in the profile's pnpm-workspace.yaml (allowBuilds: dsh-git-guard: true), because pnpm refuses to run prepare on git dependencies by default. Lock the commit (#<sha>) and only authorize source you trust.
Verify the layer before starting:
dsh --profile web --dump-config # shows a "# == dsh-git-guard" layer
Remove with dsh plugin --profile web remove dsh-git-guard.
Config
- id: git-guard
name: dsh-git-guard
config:
mode: block # block | warn | off; default block
includeUntracked: true # protect untracked files and directories
maxBackupBytes: 1048576 # hash/backup cap per protected file
backupRoot: ~/.dsh/git-guard/backups # default: harness home
mode: blockdenies a whole-file write to a protected file with a reason naming the path and the fix (targeted edit, or commit/stash first).warnlets the write proceed and attaches a notice to its result.offdisables interception while keeping the turn-end report.- Config fails loud at load: a non-integer or non-positive
maxBackupBytes, a relativebackupRoot, or a duplicate/emptyblockToolsentry throws.
How it works
- Protected set — On
turn/start, parsesgit status --porcelain=v1 -zin the repository containing the session cwd and protects every dirty path, minus files the agent whole-file-wrote earlier in this process. Protected files are hashed and copied tobackupRoot/<session>/<turn>/when they fitmaxBackupBytes; untracked directories protect their subtree by prefix. - Interception —
tools/pre-executeawaits the turn's snapshot (the first tool call of a turn can never race the hashing), resolves the target path against the session cwd with symlinks canonicalized, and denies or warns when the target is protected. A guarded write to an unclaimed file claims it as agent-owned for later turns. - Turn-end report — re-hashes each protected file and checks
git diff --quiet HEADfor tracked files:modified(uncommitted changes remain),lost(worktree equals HEAD or file gone, with the backup path), orblocked. The report is appended as a plugin-sourceuser/messagenotice, so it is logged and model-visible with no new session event type. Sessions outside a git repository stay inactive.
Model experience
The denial is a tool error naming the protected path and the fix; the turn-end report is a notice-form context whose one-line summary is bounded to 120 characters. Both are retained history for the agent and follow the reusable request prefix (append-only; they do not invalidate existing KV-cache entries).
Development
pnpm install
pnpm run build # types into lib/types, bundle into lib/index.mjs
pnpm test # unit + real-composition suite against throwaway git repos
The bundle's prepare script runs the same self-contained tsdown build, so a git install produces lib/ without a monorepo checkout.
Release
The release flow follows the antfu/starter-ts convention: bump, tag, and push locally; CI publishes to npm and creates the GitHub release.
pnpm run release # bumpp: bump patch, commit, tag v*, push
pnpm run release -- minor # or major, or an explicit version
Pushing a v* tag triggers .github/workflows/release.yml, which publishes the package to npm and generates the GitHub release from commits. The publish step needs a NODE_AUTH_TOKEN repository secret — an npm token with publish permission (Settings → Secrets and variables → Actions).
License
MIT