- Home
- Plugins
- Git & Code Review
- dsh-ssh-manager
dsh-ssh-manager
andreasseidl/dsh-ssh-manager
Manage SSH identity keys and per-host ssh config from the web settings page, so git-over-ssh works in headless server mode.
Install
dsh plugin --profile web add github:andreasseidl/dsh-ssh-managerREADME
dsh-ssh-manager
A DeepSeek Harness plugin for
managing SSH identities from the web settings page, so an unattended agent
can git clone git@host:user/repo in headless server mode.
It adds one card to Plugins → Plugin configuration → SSH and turns
what you enter there into real ssh files under $DSH_HOME/.ssh (~/.dsh/.ssh
when DSH_HOME is unset) — always separate from your own ~/.ssh.
Install
Requires Node ≥ 20 and pnpm (which dsh plugin forwards to).
dsh plugin --profile web add dsh-ssh-manager # from npm
dsh plugin --profile web add /path/to/dsh-ssh-manager # or a local checkout
That installs the package into the profile and appends it to
dsh.profile.bundles; the plugin's own cordis.patch.yml inserts its loader
row from there. Restart the harness and the card appears — no web-app
rebuild, and nothing to build in this package (lib/ ships prebuilt and
self-contained).
Don't also add a - insert: - id: dsh-ssh-manager row to the profile's user
patch: two rows with the same id are a fatal duplicate-entry error.
Alternative: symlink + user patch (for a checkout you edit in place)
ln -s /abs/path/to/dsh-ssh-manager "$DSH_HOME/profiles/<profile>/node_modules/dsh-ssh-manager"
then add the row to the profile's cordis.patch.yml:
- insert:
- id: dsh-ssh-manager
name: 'dsh-ssh-manager'
Profiles with patchReload: live pick the patch up without a restart; write it
atomically (write tmp + rename) if so.
Using it
- Open Plugins → Plugin configuration → SSH.
- First time only: the card shows a warning and greys out Add SSH host
until
~/.ssh/configimports the managed dir. Add the Include line appends that one line for you, preserving your existing config and its file mode. - Add SSH host → fill in host, URL/HostName, user, paste the private key, add any extra ssh-config options → Save.
git clone git@github.com:you/reponow works from the agent. The public key is derived to<host>.pub, ready to paste into GitHub/GitLab.
Fields. Host is the ssh Host alias, URL / HostName the machine to
connect to, User the ssh user (git for GitHub/GitLab), Extra ssh-config
options one directive per line (e.g. ProxyJump jump-host).
The private key behaves like a provider API key: write-only, never shown back or sent to the browser. A new host must carry one before it can be saved; editing an existing host shows Configured — paste a new key to replace and keeps the stored key if you leave the field blank. Renaming counts as new, since the old entry's key leaves with it.
Validation. A key must start with a PEM header (-----BEGIN OPENSSH PRIVATE KEY-----, -----BEGIN RSA PRIVATE KEY-----, …). Host names must start with a
letter or digit and use only letters, digits, ., _, - — they double as
directory and file names on disk.
What it writes
$DSH_HOME/.ssh/
config flat ssh_config; only a managed Include region is
added, everything else is preserved
config.d/<host>/
config the Host block, fenced with managed markers (0600)
<host> the private key (0600)
<host>.pub the derived public key (0644)
Each host owns its files, so concurrent editors never contend on one file.
Removing a host deletes its whole config.d/<host>/ directory. Anything the
plugin did not write — a seeded default config, known_hosts, manually placed
keys — is never touched. In the DSH container,
/etc/ssh/ssh_config.d/99-dsh-container.conf already Includes
$DSH_HOME/.ssh/config, so the managed blocks take effect for git.
Keys live in the settings document as role('secret'): never serialized into a
describing response, and written to disk with mode 0600.
Development
npm install # tsdown + schemastery, build inputs only
npm run build # host bundle (tsdown) + client wrap
src/index.js→lib/index.js— host half: schema, validation, and the reconcile that writes and cleans the ssh files. Bundled self-contained (schemastery inlined): the harness imports it with plain Node resolution from wherever it is installed, so a non-bundled copy fails to load.build.mjsrefuses to emit one that isn't.src/client/index.js→lib/client.js— browser half: the settings card in plainReact.createElement(no JSX), wrapped into the loader's closure-factory artifact.package.json'sdsh.clientdeclares it.cordis.patch.yml— the patch inserting the loader row.
The card talks to the harness through ctx.settingsScope (revision-fenced
writes), the built-in client modules (react, @deepseek-ai/dsh-client-*), and
one host route for the Include check: GET|POST /dsh-ssh-manager/ssh-import
(loopback-guarded, same-origin).
Both halves are committed prebuilt, so keep lib/ in step with src/ — the
prepublishOnly hook rebuilds and refuses to publish if that changes anything
under lib/. Host-side changes need a harness restart; client-only changes
apply on a page refresh.
License
MIT — see LICENSE.
Related plugins
dsh-web (dsh-git-graph)
zhu1090093659/dsh-web
dsh-web-ui (dsh-git-graph)
zhu1090093659/dsh-web-ui
codex-guard (dsh)
akimiya-z/codex-guard
dsh-file-review
left0ver/dsh-file-review