DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

Gist Settings — DSH Plugin for DeepSeek Harness
DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins
G

@local/dsh-gist-settings

Gist Settings

Back up and restore DeepSeek Harness profile configuration through GitHub Gists using the local gh CLI.

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:soimy/dsh-gist-settings#c61afc243b1530d600a3c742ac4668a2bf2c1868
READMECompatibilityVersions

Compatibility and provenance

Gist Settings is published as @local/dsh-gist-settings and currently resolves to version 0.2.0. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
any
Release source
github
Registry updated
9/28/2026

Versions

0.2.0stable
9/28/2026
0.1.0stable
9/27/2026

Related plugins

Loading related plugins…

Latest
0.2.0
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
any
License
MIT
Source
github
GitHub
★ 0
Weekly downloads
0
Last push
9/30/2026
View source ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue

Related plugins

More verified plugins in developer-tools.

Web App@deepseek-ai/dsh-web-appThe dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)Sdk Minimal@deepseek-ai/dsh-sdk-minimalThe standalone minimal SDK profile bundle: JSON-RPC, one DeepSeek adapter, persistent shell, and JSONL sessionsSdk App@deepseek-ai/dsh-sdk-appThe dsh SDK profile bundle: stdio JSON-RPC serving and process lifecycle over dsh-baseExperimental Inspector@deepseek-ai/dsh-experimental-inspectorExperimental cross-realm CDP hub for Host debugging and Client Runtime inspection

README

dsh-gist-settings

English · 中文

A DeepSeek Harness Cordis plugin that backs up and restores your Harness profile configuration to GitHub Gists, using the gh CLI you already have installed and authenticated.

One secret gist per profile, holding that profile's tracked config files. Upload, download, and a guarded two-way sync are exposed as agent tools, so you can drive them from a conversation.

Status

PartState
Sync engine (lib/core.js)Done
Host plugin + agent toolsInstalled and live; callable from a session
Real GitHub round-tripVerified against a real account
Test suite180 offline cases across seven suites, plus 12 live cases, all passing
CIOffline suites and repository checks on Linux, Windows and macOS, Node 20.3, 20.x, 22.x and 24.x
Client settings pageNot started — see The settings page
LicenceMIT

This code has been through two rounds of adversarial review (independent security, correctness, integration, mutation-testing and documentation audits, then a re-review of the fixes). Everything they proved is fixed and pinned by a regression test; the safety model below describes the resulting behaviour rather than the original intent.

Contributions are welcome — see CONTRIBUTING.md for the issue, pull-request and changelog conventions, and CHANGELOG.md for what has changed.

Requirements

  • Node.js 20.3+ — the engine uses node:-prefixed built-ins, AsyncLocalStorage to catch a nested lock, and AbortSignal.any to put one deadline on the global fetch that reads truncated gist content. That last one is why the floor is 20.3 rather than 20.0: on 20.0–20.2 the plugin loads and then fails the first time it has to read a file above the API's truncation threshold. .github/workflows/ci.yml runs the suites on 20.3.0 itself, so the floor is tested rather than claimed.

  • gh CLI, installed and authenticated.

    gh auth login
    

    Gists are read and written through gh api, so no extra OAuth scopes are needed beyond the default. The plugin never stores, reads, or asks for a token of its own — gh owns the credential.

    If gh is not on the PATH of the process running the Harness — common on Windows right after installing it — the plugin probes the usual install locations before giving up, and you can always name the binary explicitly with ghPath.

Install

Installing requires a session with full access or an approval prompt; plugin_manager refuses otherwise. An install can also ask for approval of package build scripts, which this bundle does not have.

plugin_manager  action: install_bundle  target: <absolute path to this directory>

That links this directory into the profile, adds the bundle to dsh.profile.bundles, and applies the patch in cordis.patch.yml. Afterwards a restart is needed before a new module generation loads.

Do not add the bundle row or the dependency to the profile by hand — install_bundle owns the selection. The row's config: block is yours to edit (see Configuration).

Uninstall

plugin_manager  action: remove_bundle  target: @local/dsh-gist-settings

The gists the plugin created are not touched. To remove those too, delete them on GitHub (the URLs are in state.json), then delete the state directory:

<stateDir>            # default: <dshHome>/gist-settings

That directory holds state.json (the profile → gist mapping), state.json.bak, every backup the plugin has taken, and a state.lock file for as long as an operation is writing. Deleting it loses the mapping and the local backups, but not the gists.

A download also creates a short-lived .dsh-gist-settings-staging-<pid>-<time> directory inside the profile it is writing to, so that the new content can be staged on the same volume before anything tracked is touched. A killed process can leave one behind; it is inert — never tracked, never uploaded, skipped when profiles are listed, and removed by the download's own finally on every normal path.

Tools

ToolDirectionWhat it does
gist_status—Reports whether gh is installed and authenticated, then lists every profile with its gist URL and sync state. Never touches profile files or the gist; it may repair a stale sync baseline in its own state file.
gist_uploadlocal → gistCreates a secret gist on a profile's first upload, then updates that same gist. Removes gist files that are no longer tracked — see the warning below. Never modifies local files. A tracked profile whose directory is gone is named in the result rather than silently skipped.
gist_downloadgist → localRestores local files from the gist. Backs up the existing local files first, and writes all-or-nothing.
gist_syncbothFast-forwards whichever side changed, restores tracked files that went missing locally, and does nothing when both sides match. Refuses to guess on a true divergence, and never propagates a deletion.

All four accept an optional profile; omitting it operates on every profile. A failure on one profile never aborts the others. Every argument is type-checked — a wrong type or an unknown key is an error, never a silent coercion, so force: "false" cannot become a forced overwrite.

The one exception to "every profile": gist_upload without a profile covers the profiles that still have a directory on this machine, because a deleted directory holds nothing to send. Such a profile is named in the result and pointed at gist_download rather than dropped from the report — silence there would read as "everything is backed up" when it is not.

Profile names are matched exactly. ALPHA is refused when the directory is alpha, because on Windows and macOS those name one directory and accepting both would back it up twice, into two gists that then diverge silently. The error names the real profile. An unknown name is refused with the list of the ones that exist, rather than failing later with a confusing "has none of the tracked files".

Status vocabulary

gist_status prints exactly these:

PrintedMeaning
not trackedNo gist exists for this profile yet.
in syncLocal and gist agree.
local changes to uploadOnly the local side changed since the last sync.
gist changes to downloadOnly the gist changed since the last sync.
DIVERGED - needs a decisionBoth sides changed. Nothing is guessed; choose a direction.
tracked files missing locally - run gist_download to restoreA tracked file is gone from disk but still in the gist.
gist deleted - the next upload recreates itGitHub returned a genuine 404 for the gist.
gist UNREACHABLE (not deleted) - retry when connectedThe gist could not be read for any other reason — offline, 401, 5xx, rate limit.
unknown - gh is unavailable, remote state not checkedgh is missing or logged out, so only the local tracking record is known.

gist_upload deletes gist files that are no longer tracked, and keeps no backup of them. "No longer tracked" means absent from the configured profileFiles — not merely missing from disk. A file added to the gist by hand (through the GitHub web UI, say) will be removed by the next upload. A tracked file that is only missing locally is a different case: the upload fails and tells you to download instead, because the gist may hold the only remaining copy. force: true overrides that and deletes it from the gist too — and the result says which files it deleted, so the two cases are never confused.

Configuration

Tunable values live in the profile patch layer, so they survive plugin upgrades. Edit the dsh-gist-settings row's config: block in your profile's cordis.patch.yml (the commented block in this repo's cordis.patch.yml is the template):

KeyDefaultMeaning
ghPathprobe PATH, then well-known install dirsPath to the gh binary. May also be an array of [command, ...prefixArgs] to reach gh through a wrapper, e.g. ['wsl', 'gh'].
profileFiles['cordis.patch.yml', 'package.json']Which files in each profile directory are tracked.
dshHome$DSH_HOME, else ~/.dshHarness home directory. A leading ~ is expanded and relative values are resolved against the process working directory.
profilesDir<dshHome>/profilesWhere profiles live.
stateDir<dshHome>/gist-settingsWhere the gist index and backups are written.

Each profileFiles entry is also the file's name in the gist and its path inside a backup, so it has to be a plain single name: no separator, no .., no absolute or drive-relative prefix, no colon, and no two entries that fold to the same name on this platform. A separator is refused because a gist is a flat collection of files rather than a tree — GitHub answers a filename containing a slash with HTTP 422 Validation Failed, and that filename is exactly what has to be sent. A name that could never be handled is rejected while the plugin loads, rather than on the first tool call.

The plugin declares no Config schema, so it validates the block itself at load time: an unknown key is rejected with the list of known keys, and a wrong type is rejected rather than silently falling back to a default. A typo therefore fails loudly instead of quietly changing which files are backed up.

The safety model

Gist layout. One secret gist per profile, described as DeepSeek Harness profile config: <name>, containing that profile's tracked files under their own names. The profile-to-gist mapping lives in <stateDir>/state.json.

A transient failure is never mistaken for a deletion. gh reports a deleted gist and an expired token the same way — exit 1 with a message on stderr. Only a genuine HTTP 404 is read as "the gist is gone"; a 401, a 5xx, a rate limit or a DNS failure yields unreachable, and both upload and sync refuse rather than mint a replacement gist that would abandon the original.

Divergence is never guessed at. The state file records the content hash of the last agreed revision as a baseline. Comparing the current local and gist hashes against it distinguishes "only local changed" (safe to upload) from "only the gist changed" (safe to download) from "both changed" (diverged, and the plugin stops). Only the tracked subset of the gist is compared, so an extra file in the gist cannot make a profile look permanently out of date.

A tracked file missing locally is restored, not deleted. It is reported as its own state, sync downloads it, and an upload refuses without force. This is the one asymmetry the tools never resolve on their own, because the gist may hold the only copy.

A deletion is never propagated, in either direction. Deleting a tracked file from the gist by hand does not delete it locally: the download keeps it, and gist_sync puts it back into the gist in the same call, so one sync reaches a stable state instead of leaving the profile reporting local changes to upload after a sync that claimed to have finished. Deleting it locally does not delete it from the gist either: the upload refuses. There is no flag that applies a remote deletion to the local copy — delete the local file yourself, then sync, if that is what you want. The one deliberate override is gist_upload with force: true, which drops a locally-missing file from the gist.

Backups before destructive writes, and an all-or-nothing write. gist_download copies the profile's tracked files to <stateDir>/backups/<profile>/<timestamp>/ before overwriting anything, including when forced. New content is then staged inside the profile directory and measured, and only once every byte has landed is it renamed into place: one atomic rename per file, which is either the old content or the new one and never a truncated mixture. If a rename still fails, the files already replaced are put back from the same bytes the backup holds, and the error says so and names the backup directory. A failed download therefore leaves the profile as it was, rather than at a revision that exists nowhere — with one deliberate exception, because the rollback undoes only what this transaction truly wrote. A file that changed since, or one the rollback cannot read back to compare, is left exactly as it is and named in the error: an edit made after the download is in no backup, so overwriting it on a guess is the one outcome that could destroy data nothing can recover. Two further honest qualifications: the rollback is best effort, so if it cannot restore a file it says which one and the backup is the recovery route; and if the state write after a successful commit fails, the files are in place and the error says that instead of claiming a lost download — the next gist_status repairs the stale baseline.

Containment — the profile, and then every tracked file inside it. Profile names are validated against the framework's own rules, and a resolved profile directory is re-checked against realpath(profilesDir), so a junction placed at profiles/<name> is refused rather than followed. Each tracked file is then held to the profile's boundary: a profileFiles entry must be a relative path with no separator, no .., no drive letter and no reserved character, and its resolved path is re-checked. A tracked file that is itself a link out of the tree, or that is a link into a sibling profile — which would let one profile's gist publish and then overwrite another profile's config — is refused. The check is repeated immediately before each write rather than once per operation, which narrows the window in which the profile directory could be swapped for a link mid-download to the rename call itself; closing it completely would need handle-relative operations that Node's fs does not expose. Note what that does and does not cover: a link that is live when it is checked is caught, and one that is dangling cannot be written through at all, but the check is still a check-then-use — the gap is narrowed, not closed.

One writer at a time, across processes. Read-modify-write cycles over state.json are serialised by an in-process queue and by a lock file (<stateDir>/state.lock) naming the pid that owns it. Two Harness hosts sharing one state directory therefore queue rather than lose each other's updates — the failure mode without it being a dropped profile record, which untracks a profile and orphans its gist, or two gists minted for one profile. A lock whose owner has exited is reclaimed at once; one written by another machine cannot be judged by this machine's pid rules, so it is judged by age instead and kept fresh by a heartbeat while it is held. Releasing a lock removes it only if it is still the one this process took. A nested call is refused with a clear error rather than deadlocking.

No interactive editor, ever. Gist reads and writes go through gh api. The obvious alternative, gh gist edit, opens $EDITOR and would hang a non-interactive plugin host.

A large file comes back whole. GitHub truncates a gist file's content in the API response above 1 MB and names the remainder at raw_url. That URL is validated to be a GitHub raw host over https, checked again after any redirect, and fetched with fetch — not gh api, which is documented as taking an API endpoint and would carry the caller's token to a host it was never configured for — under a 60 second timeout and a 64 MiB cap on the body. No credentials are sent, which is why it works: a secret gist is unlisted, not access-controlled. Node's fetch does not honour HTTP(S)_PROXY or the platform certificate store the way gh does, so a transport failure falls back to the request gh would have made; an HTTP error status does not, because a 404 has to stay a 404. Truncation is resolved only for files this profile tracks, so a large attachment someone added through the web UI neither costs a request on every status call nor can break one. test/regression.test.mjs pins the offline half of this, and test/live.test.mjs round-trips a 1.5 MB file against real GitHub to prove the rest.

No imports from the Harness installation. Tool definitions are written as plain objects matching the shape defineTool produces. This keeps the bundle immune to module-resolution changes; test/schema.test.mjs replays the Harness's own validators so the definitions cannot silently drift out of contract. The trade-off is that the Harness's version gate cannot see the plugin either, so peerDependencies pins the DSH version it was written against.

A secret gist URL is a bearer read capability. Anyone holding the URL can read a secret gist, and the tools return that URL in their output, which becomes part of the conversation transcript. Treat the URLs accordingly.

Repository layout

index.js               Host plugin: registers the four agent tools
lib/core.js            Sync engine — no Cordis dependency, directly testable
cordis.patch.yml       Bundle patch (inserts the plugin row; documents config)
client.js              Client settings page (not yet written)
locale/{en,zh}.json    Plugin display metadata for Plugin Manager cards
icon.svg               Bundle icon
test/                  180 offline cases across seven suites, plus 12 live ones
scripts/               check-changelog.mjs — validates CHANGELOG.md
.github/               Issue forms and the pull-request template

Documentation

DocumentWhat it covers
README.zh-CN.mdThis README in Chinese. The two are kept in sync; a behaviour change that updates only one is incomplete.
CONTRIBUTING.mdReporting, development setup, what each test suite proves, and the changelog, commit and release conventions.
CHANGELOG.mdEvery notable change, newest first, in Keep a Changelog form.
LICENSEMIT.

Development

npm test                # the seven offline suites (180 cases) plus the changelog and docs checks
npm run test:sync       # engine lifecycle against a fake gh
npm run test:tools      # tool layer, argument validation, failure isolation
npm run test:schema     # definitions vs. the installed Harness validators
npm run test:regression # the defects the adversarial reviews found
npm run test:safety     # containment, atomic writes, forced deletion, recovery, locking
npm run changelog:check # CHANGELOG.md structure and version consistency
npm run test:live       # opt-in: real GitHub

test/fake-gh.mjs is an in-memory stand-in for gh implementing --version, auth status, and the /gists API, with optional injection of truncated files, an untrusted raw_url host, HTTP 500s and a logged-out CLI. The sync, tools, regression and safety suites point ghPath at it, so the whole lifecycle — create, upload, download, divergence, backup, pruning, gist recreation, idempotency, recovery — runs offline with no GitHub account.

test/schema.test.mjs locates the installed @deepseek-ai/dsh-tools from process.execPath (searched first when you set DSH_TOOLS_DIR) and replays the runtime's own checks: the registration contract, the supported JSON Schema subset, argument validation, and that each tool's returned value satisfies its declared output schema. Finding no installation fails the suite rather than skipping, because a silent skip would leave npm test green with none of those checks having run; set DSH_ALLOW_SCHEMA_SKIP=1 to accept the skip deliberately.

test/live.test.mjs is opt-in because it creates a real secret gist. It works inside a throwaway DSH_HOME under the OS temp directory, so no real profile is read or written, and it deletes every gist it created even when an assertion fails. Besides the gh api round trip it covers the one path a fake cannot vouch for: a 1.5 MB file, which real GitHub truncates in the JSON response, coming back whole from the raw_url the API named.

$env:DSH_GIST_LIVE_TEST='1'; npm run test:live     # PowerShell
DSH_GIST_LIVE_TEST=1 npm run test:live             # bash

Because install_bundle links this directory into the profile, the working copy is the live plugin: edits to index.js and lib/ take effect on reload.

The settings page

A Client settings page is planned but not written. Two findings shape it:

  1. The active profile has no Web UI. The dsh-tui profile composes the terminal interface; the dsh-web-app bundle — which provides the browser surface and the settings slots — is disabled there. A settings page therefore has nowhere to render until the plugin is also installed into the web profile, or the web bundle is enabled for dsh-tui.

  2. Client-to-Host calls need a supported channel. The shipped, typed route is the settings domain (ctx.remote.settings.*, or the ctx.configForms wrapper), which requires the Host half to declare a settings namespace — and that means importing the Harness's schemastery from a workspace-installed bundle, which is unverified for a non-shipped bundle. The alternative, the raw ctx.connection.rpc channel, is fully typed but has no shipped consumer outside the API gateway. This is the open question to settle before writing client.js.

The page is intended to register into the settings.section slot (one nav entry in the Settings dialog), showing the same table as gist_status with buttons wired to the four operations.

Licence

MIT © 2026 Shen Yiming.

package.json sets "private": true on purpose: it means the package is never published to npm and exists only to be installed as a local bundle. That is orthogonal to the licence.

Review history

An adversarial review of this repository found, among other things: a gh failure that could kill the whole Harness process; every gist read error being read as "deleted", silently forking backups; and an upload that deleted the gist's only copy of a file that was merely missing from disk. All of those are fixed and covered by test/regression.test.mjs, whose cases are written to fail if the fix is reverted. The review also confirmed several things were already sound: the registration contract, the effect and disposal lifecycle, the manifest, and the divergence classifier when both sides hash the same file set.

A second review (issue #1) re-examined those fixes and found the guards stopped one level too early: the profile directory was contained but the tracked files inside it were not, a download could still half-apply, force: true was accepted and then ignored, a profile whose directory had been deleted was invisible to every bulk operation, one sync did not converge after a remote deletion, the state lock was process-local, and truncated content was fetched through gh api on an undocumented assumption. All seven are fixed.

Reviewing that round found more, including one defect it had introduced: resolving a tracked path whose parent directory was missing returned the shortened path, so downloading config/app.yml into a profile without config/ wrote the content into a file named config and reported success. Also found and fixed: containment held at the profiles directory rather than the profile, so a link to a sibling profile let one profile's gist publish and then overwrite another's config; the path was resolved once per operation, so a directory swapped for a link mid-download could redirect the write out of the tree; a symlinked profiles directory made a deleted profile look like an escape and disabled its recovery; two tracked names that fold to one file were accepted and then made every download fail; a failed state write reported a lost download and left a temp file behind; the fetch used for truncated content had no timeout, no size cap, no redirect check and no proxy fallback; a large untracked file could make the whole profile unreachable; and the prose claimed a remote deletion could be applied with gist_download force, which no code path did. test/safety.test.mjs, test/regression.test.mjs and test/live.test.mjs pin each one.