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_password_history | List an entry's previous passwords (1Password/Bitwarden-style, newest first, capped at 10; current password excluded) | | kind: card | Bank/credit-card entries: cardNumber/cardExpiry (MM/YY)/cardCvv/cardHolder; search summaries expose expiry + holder only (never the number or CVV); Bitwarden JSON export maps to a card item (type 3, brand inferred) | | vault_password_rollback | Restore an entry password to a stored history entry (current password is archived first, so it is reversible) | | vault_recovery_code / vault_verify_recovery / vault_recovery_status | One-time vault recovery code (1Password/Bitwarden-style): 32-char code shown once, only its SHA-256 hash is stored; verify possession of the code; check whether one is set | | 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_bulk_delete | Soft-delete entries matching a query/kind/tag or explicit ids; confirm: true required (dry-run by default); trashed entries are restorable | | 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_watchtower | Watchtower-style per-entry risk analysis (1Password/Bitwarden-inspired): flags short/weak passwords, keyboard sequences, embedded years, common passwords, reuse, http:// sites, missing 2FA, expiry — with a 0–100 score and good/warn/poor verdict (no secrets); entry rows show ⚠ badges | | 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_attach / vault_attachments / vault_attachment / vault_detach | Attach files to entries (private keys, certs, configs, recovery codes) — stored base64 inside the encrypted entry, encrypted at rest; list names/sizes, read content, remove | | 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_match_url | Find login entries matching a URL (Bitwarden/1Password-style: exact host, subdomain, parent domain, path-prefix; www./port normalization) with a 0–100 score — never returns the password | | 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_export_1password | Export entries as a 1Password 1PUX archive (ZIP + export.data) for import into 1Password or re-import here; item categories map to login / credit card / API credential / server | | 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; built-ins now include Wi-Fi, Server, Database, Identity, Bank account, Card (1Password-inspired) | 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 web add dsh-vaultnpm 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 web add github:Ox0400/dsh-vault#v0.1.1A 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:
1. Run the add command — it fails with an allowBuilds error and prints the exact key to allow (the line containing the repo URL, including the resolved commit hash):
``text allowBuilds: dsh-vault@https://codeload.github.com/Ox0400/dsh-vault/tar.gz/<sha>: true ``
2. Append that exact key to the profile's pnpm-workspace.yaml ($DSH_HOME/profiles/<name>/pnpm-workspace.yaml):
``yaml packages: - . allowBuilds: dsh-vault@https://codeload.github.com/Ox0400/dsh-vault/tar.gz/<sha>: true ``
3. Re-run the add — pnpm now runs the prepare script, builds lib/, 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 web add /absolute/path/to/dsh-vaultpnpm 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 web add ./dsh-vault-0.1.1.tgzThe tarball ships prebuilt lib/ artifacts, so no build step or allowBuilds is required.
dsh plugin --profile web 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: trueWith 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-vaultSecurity 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
…