Back to home

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: block denies a whole-file write to a protected file with a reason naming the path and the fix (targeted edit, or commit/stash first). warn lets the write proceed and attaches a notice to its result. off disables interception while keeping the turn-end report.
  • Config fails loud at load: a non-integer or non-positive maxBackupBytes, a relative backupRoot, or a duplicate/empty blockTools entry throws.

How it works

  • Protected set — On turn/start, parses git status --porcelain=v1 -z in 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 to backupRoot/<session>/<turn>/ when they fit maxBackupBytes; untracked directories protect their subtree by prefix.
  • Interceptiontools/pre-execute awaits 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 HEAD for tracked files: modified (uncommitted changes remain), lost (worktree equals HEAD or file gone, with the backup path), or blocked. The report is appended as a plugin-source user/message notice, 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