dsh-github-gate
Manifest validGuarded GitHub tools for DeepSeek Harness: 9 typed github_* tools (58 actions) where every state-changing action needs confirm:true plus a human approval seam, with a digest-verified self-update and a
dsh-github-gate
GitHub tools for DeepSeek Harness where a write cannot happen by accident.
9 typed github_* tools over the GitHub CLI, 61
actions in all. 29 of them are marked destructive and need
confirm: true and a human approval seam; the four write methods github_api accepts
are classified at run time.
The model's flag is intent; it is not consent.
github_pr { action: "merge", number: 42, confirm: true }
→ an approval prompt naming the pull request; "no" stops it
→ after consent: gh pr merge 42 --repo owner/name
- Argv arrays, never a shell.
ghis spawned withshell: false, so model text cannot become shell syntax — structurally, not by filtering. - One credential chain, resolved per call. DSH credential store →
GH_TOKEN/GITHUB_TOKEN→gh auth login→ git credential helper. Rotating a token applies to the next call. - Configuration in the GUI. Settings → Plugins → GitHub: paste a PAT, test it against the API, see which layer is live. The token is never displayed back.
- A branch pill in the session header: current branch, its pull request, and a switch menu that fails closed when the caller cannot be judged.
- Digest-verified self-update.
github_updatestages a release whose sha256 is checked against the digest GitHub publishes.
Host-side tools plus one small browser half. 0 runtime dependencies. No build step.
Requires DeepSeek Harness
>=0.2.0-0 <0.3.0and the GitHub CLI (gh) onPATH. Linux, macOS, and Windows.
Topics: dsh-plugin · deepseek-harness · dsh · cordis-plugin · github · gh-cli
Why this over another GitHub plugin
Tool count is a bad proxy for usefulness — a broader surface with looser gates is easier to write and harder to trust. The trade this project makes is explicit:
| Other GitHub plugins for DSH | This one | |
|---|---|---|
| Write safety | confirm: true, set by the model | confirm: true + an approval seam the model cannot set |
| Raw REST | unrestricted escape hatch | path must be relative, no .., write methods gated at run time |
| Correctness evidence | tests of its own API | 207 tests, 180 behaviour snapshots, and 84 argv shapes parsed by a real gh |
| Failure messages | HTTP code passed through | 401 / 403 quota / 403 SSO / 404 / 422 each name the fix |
| Scope | as many tools as possible | 9 tools / 61 actions, one to a gh subcommand family |
| Self-update | — | staged with a verified sha256, install left to the platform |
If you want the largest possible tool surface with no gates, this is not the right plugin. If you want the model to be able to look at GitHub freely but need a person in the loop before it changes anything, it is.
Table of contents
- Install
- Quick start
- Guidance
- The branch pill
- Settings page
- What it provides
- Updating this plugin
- Configuration
- Credential chain
- Device Flow needs your own OAuth App
- Safety model
- Background jobs
- Known limitations
- Development
Install
Two ways, both through the Harness plugin manager (plugin_manager action
install_bundle):
-
From a GitHub release — download
dsh-github-gate-<version>.tgzfrom Releases and pointinstall_bundleat the unpacked package directory, or at a local path holding it. -
From a packed tarball of your own checkout:
npm pack --pack-destination ../packages # pack OUTSIDE the source directorythen install
../packages/dsh-github-gate-<version>.tgz.
Install from a packed tarball, not from the source directory. This is not a
style preference. Installing a link: to a workspace directory produces a
junction, and the launcher's runtime resolution does not reach bare
@deepseek-ai/* packages through it — the plugin then fails with
ERR_MODULE_NOT_FOUND and refuses to activate entirely.
Pack the tarball outside the source tree: one run that packed into the source directory itself left that directory emptied once the install completed (see docs/DESIGN.md §5.8).
The tarball is a snapshot, so editing the source requires repacking and reinstalling; a restart loads the new module generation.
One-command download
The install scripts fetch the latest release, verify its sha256 against the digest GitHub publishes, and print the install path. They deliberately do not install — that step belongs to the plugin manager, which asks for approval:
# Windows
irm https://raw.githubusercontent.com/1273984347/dsh-github-gate/main/install.ps1 | iex
# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/1273984347/dsh-github-gate/main/install.sh | sh
Then install the printed path with plugin_manager action install_bundle.
Quick start
1. Is there a credential?
github_auth { action: "status" }
credential source: credentials-seam | environment | gh-cli | git-credential-helper | none
2. No credential? Establish one.
github_auth { action: "login" } # Device Flow, needs your own OAuth App
— or paste a PAT in Settings → Plugins → GitHub
— or just run `gh auth login` in a terminal; the chain picks it up
3. Something is broken?
github_auth { action: "probe" } # isolates each layer, names the failed one
4. Work on a pull request.
github_pr { action: "status" } # CI for the branch you are on
github_pr { action: "view", number: 42 }
github_pr { action: "checks", number: 42 }
github_pr { action: "merge", number: 42, confirm: true } # asks a person
The repository is taken from the workspace's git remote when repo is omitted, so
the common case needs no argument.
Guidance
Beyond the tools, this plugin contributes a prompt section and three skills that say what the tool schemas cannot: when to use which capability, what evidence a conclusion needs, and when to stop. The rules are deliberately few — a prompt section is paid for on every step — and each one names the tool it applies to.
Three workflows load on demand: github-pr-review, github-ci-triage, github-issue-to-pr.
The branch pill
client.js is the browser half: a branch pill in the session header slot
(conversation.session.header.utilities) showing the checked-out branch, the pull
request open on it, and a menu for switching branches. It is ~26 KB and it is the
only part of this bundle that renders in the browser. It reads through four
loopback HTTP routes this plugin registers on the webServer service:
| Route | Does | Without a connection service |
|---|---|---|
/branch, /branches, /diagnostics | read the workspace and branch names | served, with one logged warning |
/switch | runs git switch on the working tree | refused with 403 |
A missing service is not consent: an apparatus that cannot judge a request must not
treat "cannot judge" as "judged safe". The read routes fail open because refusing
them would disable the pill outright in a composition that legitimately has no
Connection service; /switch fails closed because its consequence is a change to
the user's working tree. client.js requires no Harness Client package and uses
only --dsw-alias-* theme tokens, so a change in a client half this plugin does not
own cannot break it.
Neither half breaks a composition that lacks the systemPrompt, skills or
webServer service: the tools keep working and the guidance simply is not
registered. The diagnostics route reports which halves are live.
Settings page
Settings → Plugins → GitHub (a tab inside the Plugins section — no nav entry of
its own, because the section owner renders each contribution as a tab).
| Shows | Does |
|---|---|
| whether a credential is configured, and which layer supplied it | — |
| the account, when the chain can identify it | — |
| the credential reference and the running plugin version | — |
| a password field | writes the token to the Harness credential store |
| Test connection | one real gh api user round trip through the same chain the tools use |
Two deliberate properties:
- The page cannot read the token back. The Host never returns it and the field is cleared on save. A settings page that can display a secret is a settings page that leaks it to every screenshot.
- Every route fails closed. These routes write a credential, so a composition
whose
connectionservice cannot judge the caller gets a403rather than a service — the same rule/switchfollows.
Rotating a token needs no restart: the chain resolves per operation.
Why peerDependencies are declared
The plugin imports @deepseek-ai/schemastery (its Config schema) and
@deepseek-ai/dsh-tools (defineTool). The runtime resolution decides what to
supply from the plugin's own manifest — profileDependencyNames reads
dependencies and peerDependencies — so a bare import that is not
declared cannot resolve, and the plugin dies at import. The declared ranges
admit the running dsh version, which is what keeps
evaluatePluginCompatibility from demanding a version exemption.
What it provides
| Tool | Covers |
|---|---|
github_auth | Credential chain status, end-to-end connectivity probe, Device Flow login, PAT login, logout |
github_repo | View, list, branches, clone, create, edit, rename, archive, delete |
github_pr | List, view, diff, checks, status, create, comment, review, ready, checkout, merge, close, reopen, edit |
github_issue | List, view, create, comment, close, reopen, edit, develop |
github_release | List, view, create, edit, upload, download, delete |
github_actions | Run list/view/log/watch/rerun/cancel/delete, workflow list/view/dispatch |
github_search | Repositories, issues, pull requests, code, commits |
github_api | Guarded raw REST for everything else (registered when allowRawApi: true) |
github_update | Check for a newer release of this plugin, and stage its verified tarball |
Updating this plugin
github_update does the two things a plugin can do safely; the other two belong
outside it.
check → is there a newer release? read-only
stage → download it and verify its digest writes one file
install → plugin_manager install_bundle platform, approval-gated
verify → confirm the new generation is live you
github_update { "action": "check" }
installed: 1.7.0 latest: 1.8.0 status: an upgrade is available
github_update { "action": "stage" }
staged: <tmp>/dsh-github-gate-updates/dsh-github-gate-1.8.0.tgz
digest: sha256 verified against the digest GitHub publishes
Then install the returned path with plugin_manager action install_bundle — it
needs approval, because that is the point at which downloaded code is allowed to
run. That is why stage reports installed: false: installing is not its job.
Whether a restart is needed depends on the install, so verify instead of
assuming. The plugin manager re-activates the entry, and a measured install
(1.7.0 over 1.6.1, 2026-10-19) served the new tool set and the new Config keys
immediately, with no process restart; an earlier install in the same environment
did not update the projected Config schema until a restart. The check that means
something is a tool or Config key that only exists in the new version — not the
installed package.json, because pnpm replaces it before the module is
necessarily reloaded. stage therefore reports next.restartRequired: "unverified"
rather than true.
Three details worth knowing:
- Every network call goes through
gh, notfetch. On a machine whose TLS is intercepted by a proxy the OS trusts but Node's CA store does not,fetchtoapi.github.comfails withUNABLE_TO_VERIFY_LEAF_SIGNATUREwhileghsucceeds. This is the same reasongithub_apiroutes throughgh api. - The digest is checked, and "no digest" is not "verified". GitHub publishes a
sha256:digest per asset; the downloaded bytes are compared against it. A release that publishes no digest is reported asunverified, not as a pass. updateCheck: trueadds a startup notice. Off by default: an activation-time network call is a surprise. When on, it runs at most once per 24 hours and logs a single line.
Credential chain
Tried in order, cheapest and most explicit first, re-read on every call so a credential stored mid-session takes effect with no restart:
- the dsh credentials store (
credentialRef, defaultGITHUB_TOKEN) GH_TOKEN/GITHUB_TOKENin the host environment- the GitHub CLI's own login (
gh auth token) - git's credential helper (
git credential fill, prompts disabled)
Machines that can already git push therefore work with no setup. To establish a
credential where none exists, use github_auth with action login.
Device Flow needs your own OAuth App
github_auth action login authorizes through GitHub Device Flow, talking only
to github.com — no broker, no secret on the machine. It requires a client id,
which is not shipped pre-filled: a client id identifies the app registration
your authorization is granted to, so a built-in one would route your access
through someone else's application that you cannot audit or revoke.
Register an App with Device Flow enabled at https://github.com/settings/applications/new, then set its client id:
- id: github-gate
name: dsh-github-gate
config:
deviceFlowClientId: "Ov23li..."
You do not need Device Flow at all if you already have gh auth login or a
GH_TOKEN — the chain above picks either up automatically.
Configuration
- id: github-gate
name: dsh-github-gate
config:
defaultRepo: owner/name # fallback when a call omits `repo`; empty detects it from the workspace remote
timeoutMs: 60000 # general command timeout
watchTimeoutMs: 300000 # watch/log actions, and any action admitted as a background job
allowDestructive: true # master switch for merge/delete/force-push
approvalForDestructive: true # ask the user before a destructive action runs
allowRawApi: true # register github_api
blockShellGit: false # deny shell-level git commit/push/merge through a tools guard
credentialRef: GITHUB_TOKEN # credentials-store reference
deviceFlowClientId: '' # your own OAuth App client id
Every field is validated at plugin load time, so a bad value fails loudly there instead of degrading silently on first use. The full catalog:
| Key | Default | Meaning |
|---|---|---|
ghPath | "" | Explicit gh executable; empty probes PATH |
gitPath | "" | Explicit git executable; empty probes PATH |
defaultRepo | "" | Fallback owner/name (or empty) when a call omits repo |
verifyGhVersion | true | Warn at activation when gh is older than minGhVersion |
minGhVersion | 2.0.0 | The minimum gh version this plugin expects |
credentialRef | GITHUB_TOKEN | Credentials-store reference; must be ENV-shaped |
timeoutMs | 60000 | Default per-command timeout (1000–600000) |
watchTimeoutMs | 300000 | Timeout for watch/log actions (1000–600000) |
maxOutputBytes | 65536 | In-memory cap before output spills to a file |
killGraceMs | 2000 | SIGTERM→SIGKILL escalation window (0–60000) |
apiBaseURL | https://api.github.com | REST root (GHES: https://ghe.example.com/api/v3) |
authBaseURL | https://github.com | OAuth host, not the API host |
deviceFlowClientId | "" | Your OAuth App client id |
deviceFlowScope | repo | Scope requested by Device Flow |
allowDestructive | true | Master switch for destructive actions |
approvalForDestructive | true | Ask the user through the approval seam before a destructive action runs |
allowRawApi | true | Register the github_api tool |
blockShellGit | false | Deny shell-level git mutations through a guard |
updateCheck | false | Log a one-line notice at startup when a newer release exists (cached 24h) |
updateRepo | "" | Repository releases are read from; empty means this project |
updateChannel | stable | stable ignores prereleases; latest takes the newest, prerelease included |
updateCacheDir | "" | Where staged artifacts land; empty uses the system temp directory |
env | {} | Extra environment for child processes |
Safety model
-
No shell, ever. Every command is an argv array spawned with
shell: false, so model-supplied text cannot be re-interpreted as shell syntax. -
Arguments are validated before execution. Tools use the official
defineToolDSL, so a wrong type, a missing required key, or an action outside the closed enum is rejected before any command is built —labels: "bug"cannot be spread into three separate arguments. -
Secrets never enter argv. PAT login pipes the token through stdin; the credential fallback keeps it in the child environment.
-
Prompts are hard-disabled (
GH_PROMPT_DISABLED,GIT_TERMINAL_PROMPT=0,GCM_INTERACTIVE=never), so nothing can hang the host process. -
Every state-changing action is gated in three layers, strongest first:
allowDestructive: false— the hard stop, checked insideexecute;- an approval ask through the official
tools/pre-executeseam, whose reason names the target so a person can judge it; confirm: true— the model's per-call intent.
Layer 2 is what closes the real gap: layer 3 alone is a flag the model sets by itself, which is intent, not consent. A composition with no approval service rejects at layer 2, so the ask never becomes a silent allow.
Which actions count. Every action that changes remote state is classified
destructive— 29 of them across the nine tools, plusgithub_apiwrite methods, which are classified at run time by theirmethodbecause onerequestaction reads as GET and writes as POST. An offline test asserts that no action is left unclassified: a new action that is neither read-only, destructive, nor in the local-only list fails the build. Three actions write the local filesystem but no remote state (repo clone,pr checkout,release download) and are deliberately outside the gate. -
Independent reads may run in parallel (
isConcurrencySafe); writes stay serialised.github_apiis never classified safe, because whether it reads or writes depends on itsmethodargument. -
Bounded processes. A wall-clock timeout and the caller's abort signal both terminate the child, escalating SIGTERM → SIGKILL after
killGraceMs. -
Bounded output.
maxOutputBytesis ONE budget across stdout and stderr combined — a run that fills both with 60 KB each spills at 64 KB, not at 128 KB. Everything past the cap goes to a spill file whose path is returned; the in-memory prefix plus the spill tail is complete output. -
github_apicannot leave the endpoint it names. The path must be relative (absolute URLs and//host/...are rejected) and must contain no..:gh apinormalizes those away before sending, so/repos/../../userwould reach/userwith the caller's credential attached. Both halves are enforced. -
REST goes through
gh api, never a direct fetch. A direct fetch would add a second, differently-configured network path (Node's CA store instead of the platform's) and fails on a machine with TLS interception the OS trusts.
Background jobs
Long actions (github_actions runLog/runWatch, github_release
upload/download) accept run_in_background: true:
github_actions { action: "runWatch", repo: "a/b", runId: "123", run_in_background: true }
-> data: { kind: "background", jobId: "github-1" }
This follows the shipped convention exactly: the flag returns the id at once, no
background execution timeout applies, and the job is collected with job_output
and stopped with job_kill. The handle is a programmatic field (data.jobId), so
PTC mode never has to parse the rendered text.
With no composed job registry the call reports background jobs unavailable and
names what to load — it does not silently fall back to running in the foreground.
The work starts inside the registry's run() callback, not before it: the registry
validates the owner and task controller first, and a rejected admission must leave
no execution resource. Spawning earlier would leak a live process into a job that
was never admitted — invisible to job_list and unreachable by job_kill.
Known limitations
- Device Flow has not completed a real authorization round trip. The polling state machine is fully covered against an injected fetch; the real github.com exchange needs your own OAuth App and a browser.
- No write action has been executed. All verification is read-only; no real resource was created, merged, or deleted.
- Only
github_authactiontokenprints a token. It exists to pipe a token into another tool, so itsstdoutis the secret; the plugin never puts one in argv, in a log line, or in a card. Treat that output as a secret. Everything else reports which layer supplied the credential and never the value. Tool cards go through the genericpresentCall/presentResultpath with a realkind(read / search). - Source edits do not take effect automatically: the installed artifact is a tarball snapshot.
- PTC mode is untested.
defineToolmakesawait tools.<name>(args)work, but this plugin has not been driven under the PTC preset.
Designs that were evaluated and deliberately not implemented — structured review evidence routing, idempotent PR creation, diff budgets, flow state — are recorded in docs/DESIGN.md §8 so the ideas are not lost with the source repositories.
By the numbers
Every value below is measured, not typed: tools/status.mjs --write produces
docs/stats.json by running the thing that owns each number, and
tools/check-docs.mjs fails when any N<!--stats:key--> claim in this file disagrees
with it. A number in prose can therefore go stale only by failing CI.
| Fact | Value | Measured from |
|---|---|---|
| Tools | 9 | tools registered by registerDomainTools |
| Actions | 61 | every domain's action table |
| Destructive actions | 29 | actions declaring destructive: true |
| Runtime dependencies | 0 | manifest.dependencies |
| Tests | 207 | test() declarations across the suite |
| Parameter assertions | 211 | tools/verify-params.mjs |
argv shapes parsed by a real gh | 84 | tools/probe-argv.mjs |
| Behaviour snapshots | 180 | tools/snapshot-behaviour.mjs |
| Injected prompt section | 2193 chars | tools/probe-guidance.mjs |
These are not a quality score. They prove that a thing was measured and still agrees with its documentation — not that the tool surface is well chosen or the verification asked the right questions. The counts are here so a reader can check them, not so the project can point at them.
Development
npm run vendor # rebuild the @deepseek-ai packages from the dsh installation
npm test # offline: no network, no gh (140 checks)
npm run verify # bundle integrity: parse, imports, manifest, entry shape
npm run probe # compile the tool DSL against the real defineTool
npm run test:live # opt-in: real read-only gh calls (14 checks)
The offline suite resolves @deepseek-ai/schemastery and @deepseek-ai/dsh-tools
from test/.vendor/, a byte-for-byte reconstruction of those packages extracted
from the dsh installation by tools/asar-vendor.mjs. Inside the Harness they
resolve through the launcher's runtime resolution instead. The tests deliberately
do not stub them — a stub would only test the stub.
It covers Config schema validation (every field defaulted; nine classes of invalid
value rejected), defineTool argument validation, compiled-schema subset
compliance, argv construction for all 58 actions, destructive-gate enforcement,
the approval ask, background-job admission, credential-chain precedence, the
Device Flow polling state machine, and the result contract asserted against real
return values.
Proving a refactor changed nothing
Tests check the properties someone thought to check. A behaviour snapshot checks everything that ran:
npm run snapshot -- > /tmp/before.json # before touching anything
# ... refactor ...
npm run snapshot:compare -- /tmp/before.json
It drives every action of every tool through four argument sets (minimal, maximal,
without confirm, with confirm) and records the exact argv, result value, and
rejection text — 176 snapshots. Timings are normalised before comparison, because
a 0 ms vs 1 ms difference is clock noise and a tool that cries wolf gets
ignored. The R4 refactor was done under this net and required 176/176 identical
between every step.
Checking the argv against the real gh
Two probes run the argv shapes against the gh binary on the machine, because
several defects shipped while every offline assertion passed:
npm run probe:argv # every argv shape this plugin builds, parsed by `gh <argv> --help`
npm run probe:arity # positional counts vs gh's own USAGE line
npm run probe:prove # what those two can and cannot catch, measured
--help parsing catches an invented FLAG NAME (run delete --yes), and the
USAGE line catches a POSITIONAL COUNT error (repo rename new-name o/n). It
cannot catch a mandatory flag that is omitted (pr review without --approve),
because gh only complains about that at execution time — so those are pinned by
exact-argv assertions in the offline suite instead. Both probes report
UNCHECKED on a machine with no gh, rather than passing: silence and success
must not look alike. npm run release runs them.
Design
docs/DESIGN.md records what was taken from which upstream project and why — including what was deliberately dropped, the two judgments that measurement overturned, and the constraints of the official DSL that only appeared when compiling against the real libraries.
docs/roadmap.md lists what is planned and — more usefully — what is not, with the reason for each refusal.
PRIVACY.md lists every destination this plugin can reach, what is stored, and how to remove it. There is no analytics and no account of its own.
MIT licensed — see LICENSE. Changes are listed in CHANGELOG.md.
License
MIT.
Comments
Loading…
Similar plugins
by vclike
DeepSeek Harness 插件:AI 操作 GitHub 的完整集成——33 个 REST/GraphQL 工具 + 进程内权限门 + 成本纪律 companion skill — DeepSeek Harness plugin: complete GitHub integration for AI agents, 33 tools, permission gate, companion
★ 0
MIT
TypeScript
Oct 3, 2026
dsh plugin --profile web add dsh-github-companionby PerryLink
Official-grade GitHub CI for DeepSeek Harness: composite action.yml, PR review bot with idempotent inline comments and a status-check gate, plus PR/issues tools with every write gated by human approva
★ 24
↓ 298/wk
Apache-2.0
TypeScript
Oct 20, 2026
dsh plugin --profile web add @perrylink/dsh-githubby w32394045-dotcom
DeepSeek Harness 的 GitHub 接入插件:设置里登录账号,agent 用它真实干活(12 个工具 + 云计算与成本护栏)。token 不经过模型上下文。
★ 0
MIT
JavaScript
Oct 6, 2026
dsh plugin --profile web add @ptfm/dsh-githubby ItQianChen
基于 GitHub CLI (gh) 为 DeepSeek Harness (DSH) 打造的现代化原生工作流集成插件。 深度打通本地 Git 仓库开发与远程 GitHub 协作(PR、Issue、Actions CI 诊断与代码审查),兼备 Agent 模型工具、人类 Slash 指令以及原生 Web UI 双重视图。
★ 12
↓ 856/wk
MIT
JavaScript
Oct 21, 2026
dsh plugin --profile web add dsh-github-flowby BOWLUNA
Structured git tools for DeepSeek Harness agents: status, diff, log, branch, commit and stash - pinned output, no shell involved, approval-gated writes.
★ 0
MIT
JavaScript
Sep 24, 2026
dsh plugin --profile web add dsh-zcode-gitby GooDAnDReaDY
GitHub operations for DeepSeek Harness: releases, tags, a guarded API pass-through, sanitized mirror publication, workflow runs and repository settings.
★ 0
MIT
JavaScript
Oct 18, 2026
dsh plugin --profile web add @goodandready/dsh-github-ops