dsh-git
Manifest validA consistent way to use git and GitHub from DSH. Commits and pushes each turn's work automatically, and redirects raw git in bash at the tool that does it cleanly — so nothing is left uncommitted or unsynced.
dsh-git
One consistent way for DSH to use git and GitHub, and the automation that makes "nothing was left uncommitted" true without a model remembering it.
A third-party bundle for DeepSeek Harness. MIT licensed — see LICENSE. Not an official DeepSeek package.
# run in the profile directory — the install target, not a global prefix
pnpm add github:stansz/dsh-git-core # the shared library, first
pnpm add github:stansz/dsh-git
Every setting has a working default, so it mounts and does something useful with no configuration at all. They are listed under Settings, and Changing a setting shows how to override one.
Three packages, three repositories
| Package | Repository | What it is |
|---|---|---|
dsh-git-core | github.com/stansz/dsh-git-core | The shared library: one place a git command is run, one state store, one recorder of the files a session changes. It registers no tool, no command and no listener. |
dsh-git (this one) | github.com/stansz/dsh-git | The git tool, /git, the guard that redirects raw git in the shell, and the automatic commit and push at the turn boundary. |
dsh-git-worktrees | github.com/stansz/dsh-git-worktrees | One git worktree per task: the worktree tool (start, list, remove) and the guard that refuses a file edit landing in a repository sitting on a protected branch. |
Install the core first. dsh-git-core is a library, not a plugin: it registers
nothing, and its bundle patch is a deliberately empty []. Installing it puts it in
the profile's node_modules, where the other two bundles find it through
lib/core-loader.mjs — a link: install does not hoist dependencies, so a bare
import cannot resolve.
Core plus dsh-git is the minimum for the everyday flow: the tool, the guard and
the automatic commit and push. dsh-git-worktrees is a separate, optional bundle with
its own settings (worktreeRoot, autoWorktree, worktrees); none of those exist on
this bundle's row. docs/install.md has the full order, the loader's
probe list, and the portability rules.
Why this exists
Raw git through bash was inconsistent: sessions left work uncommitted and
unpushed, and nothing reported it. This bundle contributes four things, and the
last two are the ones that fix it:
| Contribution | What it is |
|---|---|
ctx.tools.register() | the git tool: clone, init, status, save, publish, sync, pr, merge, finish, prune |
ctx.commands.register() | /git, printing the same state to a human |
ctx.on('tools/pre-execute') | a guard that redirects the everyday flow in bash (and pwsh) at the tool that does it consistently, and refuses the commands that discard work |
ctx.on('agent/turn-stopping'), ctx.on('session/event'), ctx.on('tools/pre-execute') | the automatic sync: at a turn boundary, or a few seconds after a turn's edits stop when no boundary arrives, commit the paths this session recorded and push what is safe to push |
The last two are deliberately not opt-in behaviours an agent has to remember. An operation that must be remembered is the original bug.
The git tool
One tool, ten actions. The model sees the full description of each; this is the shape of them.
clone— clone a repository from a URL.pathis where it lands,branchchecks out a branch at clone time. Returns the state of the clone.init— create a new empty repository.pathis where it lands,branchnames the initial branch.status— the state of a repository, or of every repository under a directory: branch, ahead/behind, changed paths, worktrees, and the open pull request when asked (pull: true). Reads only.save— commit the work this session did here. With nopaths, it commits exactly the files this session changed in this repository and reports the rest, so nothing of another session's is swept in.pathsnames files explicitly,messageoverrides the generated subject.publish— push the current branch, setting the upstream on the first push and refusing a protected branch unlessallowProtectedis true.forceWithLease: truereplaces this branch's own remote ref after a rebase — git refuses it if the remote moved since the last fetch.sync— fetch and bring the base in. With nothing else it fast-forwards when this branch has no commits of its own, and reports the divergence when it has.rebase: truereplays this branch onto the base;merge: truemerges the base in. A conflict aborts whichever ran, changes nothing, and names the paths.pr— open a pull request, or return the one already open for this branch. Never opens a second one.merge— merge the pull request by number, or the one open for the current branch, then delete the remote branch it came from when that ref still points at the merged commit. It fetchesoriginafter a successful merge (baseFetchedin the result), so a followingprunecan prove what landed without an interveningsync; a failed fetch adds a note.finish— the closing move for a unit of work: save anything dirty, publish, open or reuse the pull request, and report.prune— dry-run then remove what this tool left behind: task branches that no worktree holds and that carry no commit of their own, plus branches a merged pull request proves disposable.apply: trueremoves; a branch holding work is reported rather than deleted, unless you name it withbranches.
/git prints the same state to a human.
The guard
Raw git in the shell is inspected before it runs, for the agent's tool calls only —
a tools/pre-execute listener on bash and pwsh — so your own terminal is never
touched. Three outcomes:
- The everyday flow is redirected.
git add,commit,push,pull,fetchandrebaseare refused, each naming the action that does the same job:save,publish,sync(andsyncwithrebase: truefor a replay). - Work-loss is refused.
reset --hard/--merge/--keep,clean, a force push,branch -D,checkout -- <paths>,restore,stash drop,tag -f, the history rewrites, and the object-deletinggc --prune=nowandgit prune. Each says what is lost;git prune -n/--dry-runremoves nothing and runs. - Everything else runs. Setup and maintenance —
config,remote,tag,branch,checkout,merge,clone,worktree, reads and the rest — are not the daily flow, and denying them is how a guard becomes a wall (git config credential.helperis the repair path for a machine that cannot push). A dry run (fetch --dry-run,push --dry-run,clean -n) sends nothing and runs.
Detection is best-effort and not a security boundary: it reads the command line's
tokens and expands nothing. guardMode: warn allows everything, destructive commands
included, and reports nothing. The full refusal list, the override and wrapper cases,
and the dry-run edge cases are in docs/guard.md.
The automatic half
The outcome this bundle is named for is delivered here, and it does not depend on which commands a session used to write the files.
When a turn boundary arrives — agent/turn-stopping, or a session/event carrying
turn/end — the sync runs. When neither does, every tool call arms a settle timer:
four seconds of quiet and the sync runs on its own, and a session in an unbroken
burst of tool calls is still synced at least once a minute.
In each repository this session recorded work in, it commits only the paths this
session recorded, intersected with what is actually dirty. Anything else dirty is
left where it is and named in the report instead of being swept into this session's
commit. The commit subject says what the commit contains —
dsh: turn 3 — a.mjs, b.mjs, or dsh: a.mjs when there was no turn number — and
the body carries Assisted-by: DSH.
The commit is pushed when the branch is safe to push: one under branchPrefix, or
the branch in use when nothing is protected. A protected branch is never pushed
automatically, and a branch whose work is already in the base is not pushed either.
A skip, a refused push or a failure is written into the model's context and wakes it,
so a sync that declined to commit is not silent — unless leaving the tree alone is what
the settings asked for.
Detail, including the exact notices: docs/turn-sync.md.
Documentation
This file is the overview. The detail lives beside it:
| File | What is in it |
|---|---|
docs/turn-sync.md | the automatic half: what is committed and pushed, when it declines, how git hooks interact with it, and how to watch it |
docs/guard.md | the guard's policy: what raw git in bash is refused or allowed, and why |
docs/install.md | a fresh machine: three packages, the order they install in, and link: versus GitHub installs |
Settings
Every setting has a working default; these are the ones worth knowing.
| Field | Default | Meaning |
|---|---|---|
stateRoot | $DSH_HOME/state, else ~/.dsh/state | Where session state is recorded: the repositories this session touched, and the push backoff |
branchPrefix | dsh/ | Task branch namespace; these are what automatic push may push, and what prune may remove |
baseBranch | (empty) | Empty resolves origin/HEAD, then main |
protectedBranches | [] | The plugin's own list of branch names, matched literally against the checked-out branch. Never pushed automatically. An explicit [] lets a publish to main through without allowProtected |
guardMode | redirect | off | warn | redirect. redirect is the policy above; warn classifies and then allows everything, destructive commands included, and reports nothing |
guardAsk | ask | The one command the guard cannot classify — a wrapper whose inner text peeked a work-loss command. ask and deny both refuse it; allow accepts it |
guardTools | [bash, pwsh] | Which tools the guard inspects |
plainMode | false | Shorthand for the tool without the automation: turns turnCommit and turnPush off and protects nothing. A named key beside it wins |
turnCommit | true | Commit each turn's recorded paths |
turnCommitMessage | (empty) | Empty generates dsh: turn <N> — <paths>, or dsh: <paths> with no turn number. {turn} and {paths} are substituted |
turnPush | true | Push each turn when the branch is safe |
turnSyncScope | session | session touches only repositories this session recorded work in; workspace also scans for any dirty repository |
turnCommitMaxFiles | 200 | Skip above this many changed paths and report the repository instead |
turnSyncTimeoutMs | 20000 | Budget for one end-of-turn sync |
githubHost | github.com | Set for GitHub Enterprise |
prDraft | false | Open pull requests as drafts by default |
timeoutMs | 30000 | Per-git-command deadline |
The worktree settings — worktreeRoot, autoWorktree, worktrees — belong to the
dsh-git-worktrees bundle and are not fields of this one.
Changing a setting
Override this bundle's row in the profile's patch layer,
~/.dsh/profiles/<name>/cordis.patch.yml:
- id: dsh-git
name: 'dsh-git'
config:
protectedBranches: [main, release]
Only the fields you name are applied; every other field keeps its default. The row id
is dsh-git. Restart the Harness to load it — a plugin's row config is read when the
plugin mounts.
A row that carries a setting the schema no longer declares can fail at mount. The two
that used to live here are worktreeRoot and worktrees; they belong to
dsh-git-worktrees now, so remove them from this bundle's row rather than leaving
them behind. docs/install.md
has the exact symptom and fix.
There is no Configure page: the one that existed wrote a single toggle into this hand-maintained file, with values nobody asked for, and its effect arrived at the next start anyway. The patch layer above is how a setting is changed.
Authentication
Nothing is stored here. git runs with HOME and PATH preserved, so the
machine's own credential helper resolves credentials — on this machine that is
osxkeychain from the system gitconfig. GitHub API calls use
GH_TOKEN → GITHUB_TOKEN → gh auth token --hostname <host> — the first two need
nothing installed — read lazily, never logged, never written to disk, and redacted from
every error. Neither half is interactive: prompting is off (GIT_TERMINAL_PROMPT=0, no
askpass), so nothing can block on a question with nobody to answer it — and a credential
embedded in a remote URL is dropped wherever a remote is reported, so status shows the
host and the path, never the userinfo.
The two halves share nothing: a machine can have a working git credential and no GitHub
token, or the reverse. Reading a public repository needs no credential at all, so a new
machine looks healthy until its first push, and GH_TOKEN never helps a push. A push
or a fetch that fails for want of a credential is reported with the fix: a fetch
states it as next:, while for a push the note the turn ends with and the record
status prints both name a credential helper, an SSH remote, or — when the GitHub CLI
is present — gh auth setup-git. A failure is not remembered: the chain is asked
again on the next call, so gh auth login in a terminal is picked up without
restarting the Harness.
docs/install.md is the
checklist for a machine that has never logged in.
GitHub is talked to over REST rather than through gh, because structured
status codes beat matching error prose — and because
gh pr merge --delete-branch does
local branch cleanup in the current directory's repository, matching the branch
by name only, which can delete an unrelated checkout's unpushed commits.
Tests
node --test from the repository root: no install step, no network, no GitHub
token. The tests build real repositories with the real git binary, drive the
actions, and check the repository actual state afterwards; the GitHub API is a
local stand-in that performs a real merge in a bare origin, so the pull-request
workflow is exercised against real git refs. What each file pins, and what the
fakes are allowed to fake, is in docs/testing.md.
Requirements
git on PATH. gh is optional and only used as a token source; without it, set
GH_TOKEN. Node 18+ for global fetch. The one package dependency is
dsh-git-core, resolved at runtime through lib/core-loader.mjs;
@deepseek-ai/schemastery is declared but loaded through createRequire probes, so
a profile that cannot resolve it still mounts with every setting at its default.
On the Harness side it declares tools, commands and sessions in inject, so
a Harness without those leaves the row pending rather than half-mounting it. It
reaches for agents and workspaceChanges defensively — a build without them
still works, with a less precise idea of which files a turn changed — and registers on
tools/pre-execute, agent/turn-stopping and session/event.
Comments
Loading…
From the same category
by ccch1mneyyy
DSH 官方公众号收录的 TUI 补位插件:Claude Code 风,鲸鱼顶栏/实时状态/流式思考/双击 Esc 回滚/上下文进度+TPS。npm 一键装。 DSH official WeChat featured TUI plugin — Claude Code style: whale bar, live status, streaming thoughts, double-Esc rol
★ 4.8k
↓ 11.9k/wk
MIT
TypeScript
Oct 18, 2026
dsh plugin --profile terminal add @deepseek-harness-tui/dsh-tuiby strukto-ai
The World's First Virtual Terminal for AI Agents
★ 3.7k
↓ 374/wk
Apache-2.0
TypeScript
Oct 18, 2026
dsh plugin --profile agent add @struktoai/mirage-dshby bowenliang123
The best DeepSeek Harness plugin for context insight and management, with context dashboard / browser / sidebar and context command, for context statistics, composition, breakdown, evolution details,
★ 2.3k
↓ 52.1k/wk
Apache-2.0
TypeScript
Oct 18, 2026
dsh plugin --profile web add dsh-contextby huiliyi37
官方 DeepSeek Harness 的交互式终端 UI 插件:自研 ANSI 极简交互渲染、流式 Markdown/工具卡、16+ 主题、slash 命令与选择器、输入历史与本地偏好持久化、LSP 诊断、memory记忆,很丝滑的开发体验。
★ 287
↓ 493/wk
Apache-2.0
TypeScript
Sep 30, 2026
dsh plugin --profile terminal add @huiliyi37/dsh-tianshu-tuiby Hilbert-beinghappy
面向 DeepSeek Harness 的 Claude Code 风格终端界面,支持 Windows、macOS 与 Linux,兼容透明终端、VS Code 主题和自定义配色。
★ 196
↓ 108/wk
MIT
TypeScript
Oct 13, 2026
dsh plugin --profile web add seekttyby c3cvld7aag
富江DSH完美破甲|富江破甲 DSH 本地插件,内置提示词规则,支持 Windows 一键安装、环境自动准备与一键卸载,全中文使用说明。
★ 121
JavaScript
Oct 3, 2026
dsh plugin --profile web add fujiang-armor