dsh-vault — Encrypted Credential Vault for DeepSeek Harness
English | 中文
dsh-vault is a security-focused plugin for DeepSeek Harness that stores sensitive credentials — usernames, emails, phone numbers, passwords, TOTP secrets, and developer credentials like SSH connections, API keys, secrets, and OAuth access/refresh tokens — encrypted at rest, and exposes them to the model through CRUD, search, password generation, and TOTP tools, plus a Settings UI page.
Security & Implementation
- Zero external crypto dependencies: everything is built on Node's built-in
node:crypto(AES-256-GCM authenticated encryption, scrypt key derivation, RFC 6238 TOTP). - Master password: every entry is encrypted with a 256-bit key derived via
scrypt(master password, salt)and AES-256-GCM. The key never touches disk; after unlock it is cached in-process and re-derived on restart. - Tamper-evident: GCM auth tags plus a fixed-plaintext verification envelope — a wrong master password or modified ciphertext fails immediately, never returning garbage.
- No plaintext at rest: the on-disk document contains no plaintext credentials; each entry uses an independent random nonce.
- Atomic writes: reuses the harness
writeFileAtomic+ file lock; in-process writes are serialized, cross-process writers take the lock. - Search never leaks:
vault_searchreturns summaries only (id/title/kind/username/email/phone/host/port/url/tags) — never passwords, keys, tokens, or TOTP secrets. Full credentials are readable only via explicitvault_getby id.
Entry Model
Each record has a title, an optional kind, and any combination of fields:
| Field | Description |
|---|---|
kind |
login (default) / ssh / api-key / secret / oauth / custom |
username / email / phone |
Account identity |
password |
The password |
host / port |
SSH host and port (e.g. db.internal / 2222) |
privateKey |
SSH private key (PEM) |
apiKey |
API key |
secret |
Generic secret (client secret, shared secret, …) |
accessToken / refreshToken / expiresAt |
OAuth token pair and expiry (epoch millis) |
otpSecret |
TOTP secret (bare Base32 or otpauth:// URI) |
url / notes / tags |
Metadata |
fields |
Arbitrary key/value pairs (e.g. {"region": "us-east-1"}), searchable |
Tools
| Tool | Purpose |
|---|---|
vault_add |
Add an entry (any combination of fields; empty strings/arrays are ignored) |
vault_get |
Read a full entry by id (including all secrets) |
vault_search |
Search titles/categories/usernames/emails/phones/hosts/ports/URLs/notes/tags/custom fields (incl. numeric/boolean/nested values; whitespace-separated terms OR-match); optional createdAfter/createdBefore epoch-millis filters; returns secret-free summaries; limit must be an integer 1–100 |
vault_update |
Update fields by id (unprovided fields kept; empty string clears a field; title is renamable; rotationDays: 0 clears rotation = never rotate) |
vault_compare |
Compare two entries field by field (onlyA/onlyB/differ/equal) — field names only, never secret values |
vault_rename |
Rename an entry in one call (shortcut for vault_update) |
vault_delete |
Soft-delete an entry (moves it to the trash, still encrypted on disk) |
vault_restore / vault_purge / vault_restore_recent |
Bring a trashed entry back / purge it / undo the last delete |
vault_lock / vault_unlock |
Explicitly lock the vault (wipe the in-memory key) / re-unlock it |
vault_totp |
Generate the current 6-digit code for a stored otpSecret (or a bare Base32 / otpauth URI) |
vault_generate_password |
Generate a strong random password (length/classes/grouping) or a memorable passphrase (passphrase: true, EFF-style word list, words/separator/wordDigits) |
vault_strength |
Zero-dependency password strength estimate (score 0–100, weak/fair/strong/very strong) |
vault_rekey |
Upgrade the vault to fresh scrypt KDF parameters in place |
vault_backup |
Timestamped encrypted backup with retention; optional note |
vault_import_csv |
Bulk-import credentials from a CSV file (custom columns become fields; overwrite: true merges fields into existing entries instead of duplicating) |
vault_apply_tags |
Bulk add/remove/replace tags on every entry matching a query (dry-run supported, no secrets) |
vault_totp_uri |
Build an otpauth:// provisioning URI for a stored or bare TOTP secret |
vault_switch / vault_list |
Switch the active vault by name / list available vaults |
vault_rotation |
Report expired / due-for-rotation / expiring-soon credentials; soonWindowDays (1-90, default 7) tunes the soon horizon (no secrets) |
vault_health |
Vault health scan: weak/reused passwords, missing 2FA, insecure http:// sites, and an overall security score (0–100) |
vault_breach_check |
Watchtower-style breach scan against Have I Been Pwned (k-anonymity: only the SHA-1 prefix leaves the machine), with an offline common-password fallback |
vault_integrity |
Verify the on-disk vault file decrypts correctly and matches the in-memory store |
vault_merge |
Merge one entry into another; keepSource: true keeps the source after merging |
vault_quick_add |
Fast capture (title + one secret) with optional tags/notes |
vault_expiry |
Set/clear expiry (expiresAt: 0 removes it) |
vault_stats |
Overview counts incl. trashCount (no secrets) |
vault_verify |
Verify one entry or audit every entry (all: true) for per-kind completeness, port/expiry sanity (no secrets) |
vault_duplicates |
Find duplicate groups: mode = both (default) / title / content (no secrets) |
vault_report |
Printable inventory with expiry/rotation columns and a stats footer (no secrets) |
vault_export / vault_import |
Portable encrypted backup/migration of the whole vault (separate export password) |
vault_backup / vault_backup_now |
Timestamped encrypted backup named <vault>-backups-YYYY-MM-DD_HH-MM-SS-<hex>.json (owning vault + date visible); retention pruning keeps the newest N |
vault_restore_backup |
Restore from a backup: mode: "merge" (default) copies the backup entries INTO the current vault so they appear in the entries list; mode: "replace" overwrites the whole vault with a safety snapshot first |
vault_vault_rename / vault_vault_delete |
Rename a named vault (file moves, active session follows) or permanently delete one (default vault protected) |
vault_fill |
Find the entry matching a host/URL/username/title and return its credentials |
vault_env |
Render env-flagged entries (tags contain env) as KEY=VALUE lines |
vault_export_bitwarden / vault_import_bitwarden |
Bitwarden/Vaultwarden JSON interop (full field mapping, overwrite support) |
vault_import_bitwarden_encrypted |
Decrypt a Bitwarden password-protected JSON export (PBKDF2/Argon2id + HKDF → AES-256-CBC + HMAC) and import it; pass the export passphrase |
vault_import_manager_csv |
Password-manager CSV auto-detected by header: Bitwarden (login_uri/login_username/…), 1Password 8, Dashlane, NordPass, Keeper, LastPass (fav/grouping/extra); dryRun preview |
vault_import_kdbx |
KeePass KDBX: 3.1 and 4.x, AES-KDF or Argon2 (RFC 9106), AES-256-CBC or ChaCha20 payload, keyfile support |
vault_import_1password / vault_import_1pif |
1Password 1PUX (ZIP) and legacy 1PIF text exports |
vault_import_enpass |
Enpass JSON export (folders → tags, typed fields, TOTP) |
vault_import_keepass_xml |
KeePass 2.x XML export (plaintext or ******** masked values) |
vault_import_chrome / vault_import_keychain |
Import passwords from Chrome's Login Data (macOS keychain / Linux keyring or peanuts / Windows DPAPI) or the macOS Keychain (internet passwords inet by default — the ones that actually back website logins — or generic genp via classes; session cache + preview, no prompt spam); every file import supports dryRun preview |
vault_import_firefox |
Firefox profile import (logins.json + key4.db, NSS 3DES / PBES2-AES, primary-password aware) |
vault_search_system |
Search Chrome / Keychain for sites & usernames — never exposes passwords |
vault_session_open |
Open a real headed browser window at a URL so the user can log in manually (password, 2FA, captcha) — the portable way to capture login state for sites that block embedding |
vault_session_collect |
Collect every cookie of an open browser session (incl. HttpOnly) and save it as a cookie entry |
vault_session_import |
Save session cookies from pasted JSON (devtools export shape) or a raw Cookie header string — the no-browser alternative |
vault_session_import_file |
Import a Netscape cookie-jar file (curl -b / wget / browser-extension export; the same format vault_session_export writes) |
vault_session_list |
List saved login sessions with cookie counts, expired and expiring-in-7d counts (no values) |
vault_session_export |
Export a saved session as a Cookie header value, a Netscape cookie-jar file (curl -b), raw JSON (Playwright addCookies shape), or a ready-to-run Playwright snippet |
vault_session_close |
Close an open browser login session (collected cookies stay in the vault) |
vault_session_prune |
Remove expired cookies from a saved session (session cookies are kept); preview: true reports without writing |
vault_copy |
Copy an entry (secrets included) into another named vault |
vault_templates |
Built-in + user-defined templates (save/list/remove), KeePassXC-style |
Typical workflows: store an SSH credential (kind: ssh + host/port/username/password or privateKey) and have the model vault_search for the host then vault_get the connection details; keep api-key/oauth entries for API-gateway access/refresh token rotation.
Installation
dsh-vault is a bundle (a package declaring dsh.bundle): once installed into a profile, its cordis.patch.yml automatically inserts the vault plugin row (referenced by package name dsh-vault; the master password is injected via the DSH_VAULT_PASSWORD environment variable). The package ships a self-contained build script — git installs compile lib/ automatically.
All four install paths below are verified end-to-end (install → bundle layer recognized → plugin activates with all 7 vault_* tools registered → real vault_add/vault_get round trip → uninstall removes the layer):
| Path | Command | Build needed | allowBuilds |
|---|---|---|---|
| npm | add dsh-vault |
no (prebuilt lib/) |
no |
| GitHub | add github:Ox0400/dsh-vault#v0.1.1 |
yes (prepare) |
yes (first run) |
| local path | add /abs/path/to/dsh-vault |
no (link to built source) | no |
| tarball | add ./dsh-vault-0.1.1.tgz |
no (prebuilt lib/) |
no |
Option 1: Install from npm (easiest)
dsh plugin --profile demo add dsh-vault
npm packages ship prebuilt lib/ artifacts — no allowBuilds, no local compilation, install and go. Set the master password before launching:
export DSH_VAULT_PASSWORD='your strong master password'
Option 2: Install from GitHub (pin a tag or commit)
dsh plugin --profile demo add github:Ox0400/dsh-vault#v0.1.1
A git install fetches sources, not built artifacts, so the prepare script builds lib/ at install time. pnpm ≥10 blocks git dependencies from running build scripts by default. The verified flow:
Run the
addcommand — it fails with anallowBuildserror and prints the exact key to allow (the line containing the repo URL, including the resolved commit hash):allowBuilds: dsh-vault@https://codeload.github.com/Ox0400/dsh-vault/tar.gz/<sha>: trueAppend that exact key to the profile's
pnpm-workspace.yaml($DSH_HOME/profiles/<name>/pnpm-workspace.yaml):packages: - . allowBuilds: dsh-vault@https://codeload.github.com/Ox0400/dsh-vault/tar.gz/<sha>: trueRe-run the
add— pnpm now runs thepreparescript, buildslib/, and installs.
Pin a tag/commit so a later upstream push cannot silently change what runs on install. Treat the allowance for what it is: permission to execute that package's code on your machine at install time — only grant it to sources you trust.
Option 3: Install from a local path
dsh plugin --profile demo add /absolute/path/to/dsh-vault
pnpm links the checkout into the profile; the bundle is recognized as long as lib/ exists (run pnpm build in the checkout first if needed).
Option 4: Install from a tarball
npm pack && dsh plugin --profile demo add ./dsh-vault-0.1.1.tgz
The tarball ships prebuilt lib/ artifacts, so no build step or allowBuilds is required.
dsh plugin --profile demo remove dsh-vault uninstalls (removes both the dependency and the layer).
Configuration
| Option | Description |
|---|---|
masterPassword |
The master password inline (appears in cordis.yml; not recommended) |
masterPasswordEnv |
Environment variable name holding the master password (recommended) |
path |
Vault file path; defaults to $DSH_HOME/vault/default.json |
name |
Vault name for the default path (e.g. name: work → $DSH_HOME/vault/work.json) |
accessMode |
Access policy for the model tools. Three states: readonly (mutations rejected on tools + UI), ask (default — reads free, every add/update/delete goes through the harness approval channel so the user confirms each write), or auto (automatic read-write, no per-call prompt). The Settings UI offers this exact three-way choice and persists it to <vault dir>/access.json. |
autoCapture |
false (default). When true, the system prompt instructs the model to detect credentials shared in conversation and — per user preference — offer to save them with vault_add. |
lockTimeoutSeconds |
Auto-lock: after this many seconds of inactivity the vault re-locks (key wiped) and every read/write requires vault_unlock. 0/absent disables. |
exportPasswordEnv |
Environment variable holding the export/import password for vault_export/vault_import (never pass it as a model argument). |
backupRetention |
How many encrypted backups to keep (default 10); vault_backup prunes older copies. |
Example:
- id: vault
name: dsh-vault
config:
masterPasswordEnv: DSH_VAULT_PASSWORD
accessMode: ask
autoCapture: true
With autoCapture: true, when you share a credential in chat (e.g. "my npm token is npm_…"), the assistant offers to store it; on your consent it calls vault_add immediately. With autoCapture off, credentials are only saved when you explicitly ask. The Settings UI shows the current mode (read-only / ask-before-write / automatic read-write) with a dropdown to switch it, an auto-capture toggle (detect credentials shared in chat → offer to save), a kind filter, a health & rotation summary, a trash view, and masked secret fields with a Show/Hide toggle.
The vault is created automatically on first tool use; every launch re-unlocks with the master password. Forgetting the master password = permanent data loss (no backdoor — by design).
Development
Clone and develop locally:
git clone git@github.com:Ox0400/dsh-vault.git
cd dsh-vault
pnpm install # installs devDependencies (typescript/tsdown/vitest, …)
pnpm build # builds host lib/*.js and the browser bundle lib/client.js
pnpm test # runs the 41 vitest tests
Tests need harness peer packages such as
dsh-llm/dsh-system-prompt; inside the harness monorepo these resolve via workspace links.
Common commands:
pnpm test # unit + integration tests (vitest, 41)
pnpm typecheck # tsc -p tsconfig.json --noEmit
pnpm build # = build:host (tsc) + build:client (tsdown)
npm pack # optional: tarball for `dsh plugin add ./dsh-vault-0.1.1.tgz`
All 41 tests pass (crypto / TOTP / password generation / store CRUD / gateway / integration).
Packaging & Publishing
This package is a standard npm bundle:
dsh.bundle.patch→cordis.patch.yml(the layer applied automatically when a profile lists this bundle)dsh.client→ browser-side declaration (exports["./client"]points atlib/client.js)preparescript → self-contained build on git install (tschost +tsdownclient)- Runtime dependencies are all
peerDependencies(provided by the host harness — no duplicate instances)
Distribution options:
npm pack # tarball → dsh plugin add ./dsh-vault-0.1.1.tgz
npm publish --access public # registry → dsh plugin add dsh-vault
Security Boundaries & Known Limitations
- Vault strength is bounded by master-password strength; use ≥ 16 characters of high entropy.
- scrypt cost parameters (N=32768, r=8, p=1) are persisted in the document and can be raised in future versions; old documents remain decryptable.
- Plaintext credentials exist only in process memory and during explicit
vault_getreads;vault_search/vault_updateoutputs never contain passwords, keys, or tokens. Secrets returned byvault_getenter that tool call's result (model context) — callers should avoid repeating them in conversation. - This plugin targets single-machine / personal deployments; team-shared vaults are out of scope.
No comments yet. Be the first to write one.