ZhenHuangLab
dsh-sync
No description
- Stars
- 0
- Language
- TypeScript
- Created
- Aug 15, 2026
- Updated
- Aug 15, 2026
Introduction
dsh-sync
Policy-driven DeepSeek Harness config sync. This is not a thin git pull of $DSH_HOME.
A dual-face DSH plugin:
- Host — sidecar Git under
$DSH_HOME/.dsh-sync/, namespace-projected settings, secret scan, journaled apply,/synccommand, fenced HTTP/SSE API - Web — theme-independent
settings.section("Git Sync") plus a/syncpopupSelectdecoration
Pinned to DSH 0.1.0-rc.6.
Install
dsh plugin --profile web add dsh-sync
# optional, if /sync should exist in headless too:
dsh plugin --profile headless add dsh-sync
Then restart dsh web. Open Settings → Git Sync.
From a checkout:
dsh plugin --profile web add /absolute/path/to/dsh-sync
The package declares dsh.bundle.patch so the CLI appends it to dsh.profile.bundles. The client bundle is exports["./client"] (lib/client.js) in the ModuleLoader factory format.
What v1 syncs
| Class | Items | Apply |
|---|---|---|
| Safe / live | Selected settings namespaces as payload/settings.namespaces.yaml (user layer, secrets and local-only paths stripped) | ctx.settings.mutate with expectedRevision |
| Restart | profiles/{web,headless}/package.json | Explicit only; shows install/restart follow-up. Does not run pnpm. |
| Executable | profile/home cordis.patch.yml, user .agent-presets/*/{preset.yml,agent.cordis.yml,tool-bootstrap.mjs} | Explicit risk confirmation naming files + remote |
Never: .credentials.yaml, sessions, storages, node_modules, generated cordis.yml, .dsh-sync itself, .env*, shipped/system presets, symlinks, gitlinks.
baseURL is not treated as machine-local unless you add it under local-only paths. Tailscale CPA endpoints are portable on purpose.
A checkout that still looks like my-dsh-config (files at repo root) can set Payload root to “repository root”.
Cloning a remote that has no .gitattributes is supported. Push writes sidecar hygiene files if they are missing and never git adds a path that is neither on disk nor already tracked. Under systemd, plugin-triggered relaunch does not spawn a second process — Restart=always brings the unit back.
Commands
/sync status
/sync check
/sync diff
/sync pull # safe settings only; executable stays pending
/sync push # fast-forward only
/sync doctor
/sync recover
Bare /sync in the Web client opens the shared popup (status / check / safe pull / push). Full executable review is on the settings page.
Operator CLI (no compose): dsh-sync status|check|doctor|recover.
Security
- Allowlist by construction. Deny names in
.gitignoreare defense in depth only. execFileonly.GIT_TERMINAL_PROMPT=0.core.hooksPathis an empty plugin-owned directory.- Origin URL must match local config before fetch/push. No force-push. No Git secret in plugin config.
- HTTPS remotes with userinfo are refused. Scanner findings name path + rule, never the matched value.
- Web API is loopback + same-origin by default. Mutations require
X-Dsh-Sync-Request: 1and JSON. Non-loopback needs explicittrustedHosts. - Applying patch/preset files is a supply-chain action (they may contain
!!js). v1 never auto-applies them.
If a secret was already pushed: rotate first, then rewrite history yourself. This plugin will not.
Theme contract
The settings section binds only --dsw-alias-* tokens and official primitives (Button, Input, StateDot, DisclosureRow, DiffBlock, RiskConfirmation). It does not import ui-theme, read data-ds-dark-theme, or use color literals. pnpm run lint:theme enforces that.
Develop
pnpm install
pnpm test
pnpm run lint:theme
pnpm run build
Architecture notes live in docs/.