npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-vault

v1.10.74

Published

Encrypted credential vault for DeepSeek Harness: store and retrieve usernames, emails, phone numbers, passwords, TOTP secrets, SSH/API-key/OAuth developer credentials through model tools and a Settings UI page.

Downloads

10,136

Readme

dsh-vault — Encrypted Credential Vault for DeepSeek Harness

License: MIT npm version GitHub Release npm downloads GitHub issues Listed: awesome-dsh-plugin Listed: awesome-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_search returns 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 explicit vault_get by 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 (maxBackups, default 10) | | 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 — see Environment export | | 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 — requires the optional playwright-core | | 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-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 web 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:

  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):

    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):

    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-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 web add ./dsh-vault-0.1.1.tgz

The 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).

Command line

The plugin hands the vault to the assistant. The bundled dsh-vault command hands the same vault to shells and scripts, so a skill's child process can read a secret without the plaintext ever entering the model's context.

0. First: which command can you actually type?

Nothing provides a dsh-vault binary until the package is installed — the plugin is a Cordis bundle, and dsh has no plugin-subcommand registry (the launcher parses only its own flags; dsh plugin … just forwards to pnpm, so dsh web exec … hands its arguments to the web app and fails with error: too many arguments). Pick your row before reading the examples:

| your setup | the command | |---|---| | source checkout (git clone + pnpm build) | pnpm vault — or node lib/cli.js, or ./lib/cli.js | | plugin installed into a profile | pnpm dsh plugin --profile web exec dsh-vault | | globally installed (npm i -g dsh-vault) | dsh-vault | | nothing installed | npx dsh-vault |

# source checkout — this repository, right now:
pnpm vault list

# after a global install (or via npx) the same commands are:
dsh-vault list
npx dsh-vault list

Every example below is written as dsh-vault …; substitute the form from your row (pnpm vault … in a checkout).

1. What env is for

get fetches one value and needs no tag. dsh-vault env is for the other case: a script that needs several secrets at once. It prints the entries you tagged env as shell assignments, so the script picks them up itself:

$ dsh-vault env
DEMO_API_KEY='sk-demo-123'
OTHER_PASSWORD='pw-not-exported'

eval "$(dsh-vault env)" reads those assignments into the current shell — the usual reason to reach for it:

eval "$(dsh-vault env)"          # KEY=VALUE lines become shell variables
echo "$DEMO_API_KEY"

Two things worth knowing:

  • Only env-tagged entries are exported. With none tagged you get dsh-vault: nothing to export — no entry carries the "env" tag.; tag one in the UI (the entry's tags field) or with vault_update { id, tags: ["env"] }.
  • The name is derived from the title and field (DASHSCOPE + apiKey → DASHSCOPE_API_KEY) unless the entry sets envKeys — see Environment export.

Names — there are only three, and two of them are the word env on purpose:

| name | what it is | meaning | |---|---|---| | env | an entry tag | which entries dsh-vault env exports; nothing is exported without it | | env | the command | prints exactly the entries carrying that tag | | envKeys | an entry field (optional) | pin exact names, e.g. ["DASHSCOPE_API_KEY"]; the old singular envKey is accepted as a one-element shorthand |

Nothing else is called env*: list --json / show report envKeys (the names an entry exports — what get accepts), envTagged (whether env includes it) and hasTotp (whether totp can produce a code for it).

2. The commands

dsh-vault list                             # grouped: what `env` exports vs the rest
#   Exported by `env` (tag "env") — 1 entry:
#     a1b2c3d4-…  api-key   DASHSCOPE        → DASHSCOPE_API_KEY
#   Not exported (no "env" tag) — 1 entry; `get <name>` still works for it:
#     9f8e7d6c-…  api-key   Example Billing  → EXAMPLE_BILLING_API_KEY
dsh-vault get my-entry                     # the entry's primary secret, stdout only
dsh-vault get my-entry --field apiKey      # one named field
dsh-vault get my-entry --mask              # confirm it exists without printing it
dsh-vault show my-entry                    # non-secret metadata as JSON
dsh-vault totp my-entry                    # the current TOTP code (digits only)
dsh-vault totp my-entry --json             # + digits, period, secondsRemaining
dsh-vault env                              # env-tagged entries as KEY=VALUE
dsh-vault env --mask                       # human view, both sections as KEY=VALUE:
#   ## exported items
#   NPM_REGISTRY_API_KEY=npm_***
#   ## unexported items
#   EXAMPLE_BILLING_API_KEY=***
dsh-vault export-env .env                  # …or write them to a 0600 file
dsh-vault verify                           # check the master password, nothing on stdout

Secrets go to stdout and everything else (progress, errors) to stderr, so $(dsh-vault get …) and pipelines behave. Exit codes: 0 ok, 1 runtime error, 2 usage error.

totp exists so a script can automate two-factor login without ever handling the long-lived secret: code=$(dsh-vault totp my-entry) prints six digits and nothing else. get my-entry --field otpSecret would print the secret instead — use it only when a tool genuinely needs to enrol the secret elsewhere. --json adds secondsRemaining, which is what lets a caller wait for a fresh window rather than race an expiring code.

3. Shell quoting: not export $(…)

eval "$(dsh-vault env)" is right; export $(dsh-vault env) is wrong. Command substitution splits on whitespace and does not strip the quotes the CLI emits, so a value containing a space breaks into the wrong words and every value keeps its literal quotes:

$ export $(dsh-vault env); echo "$WITH_SPACE"
['pass]                     # split at the space
bash: export: `here': not a valid identifier
$ eval "$(dsh-vault env)"; echo "$WITH_SPACE"
[pass word here]            # read as intended

In a script, prefer the file form — no eval at all:

dsh-vault export-env .env && set -a && . ./.env && set +a

4. The master password

--password-stdin, then $DSH_VAULT_MASTER_PASSWORD (or $DSH_VAULT_PASSWORD), then an interactive prompt that hides what you type (the terminal is switched to raw mode; verify with tests/e2e/cli-prompt.mjs). It is not read from the plugin's config file, so the CLI never depends on (or leaks) whatever the profile patch holds:

DSH_VAULT_MASTER_PASSWORD=… pnpm vault list
printf '%s\n' "$PASSWORD" | pnpm vault list --password-stdin

5. Install details

Source checkout:

git clone [email protected]:Ox0400/dsh-vault.git && cd dsh-vault
npm install && npm run build            # builds lib/, including executable lib/cli.js

pnpm vault list                         # the repo's own entry point (package.json script)
node lib/cli.js list                    # same thing, spelled out
./lib/cli.js list                       # the file is executable too
npm link                                # …or put a real `dsh-vault` on PATH

To develop the plugin against a profile you also link the checkout into the profile and add a patch layer that inserts it (how this repository is normally developed); the CLI needs none of that — it reads the vault file directly. A hand-linked plugin has no .bin/dsh-vault shim, so pnpm … exec reports Command "dsh-vault" not found unless you install it as a dependency (pnpm dsh plugin --profile web add dsh-vault) or create the shim by hand:

ln -sf ../dsh-vault/lib/cli.js ~/.dsh/profiles/web/node_modules/.bin/dsh-vault

npm install:

pnpm dsh plugin --profile web add dsh-vault          # into a profile (plugin + CLI together)
pnpm dsh plugin --profile web exec dsh-vault list    # --profile is required

npm i -g dsh-vault && dsh-vault list                 # globally
npx dsh-vault list                                   # or not at all

The read commands (list, get, env, show, totp, verify, export-env) depend on Node alone, which is why the global and npx forms work. Only writing to a vault needs the harness runtime, and the plugin always has it.

Two more caveats on the profile route: its .bin shim can be pruned by a later pnpm install, and pnpm dsh plugin --profile web add dsh-vault reconciles dsh.profile.bundles from the package's dsh.bundle declaration — if your own patch layer also inserts the plugin, that mounts it twice, so remove the patch row first.

Privacy of the breach check

vault_breach_check (and the security page) can check passwords against Have I Been Pwned. Exactly one thing leaves this machine:

GET https://api.pwnedpasswords.com/range/<FIRST 5 HEX CHARS OF SHA-1(password)>
Add-Padding: true

The response lists every suffix in that bucket (hundreds of hashes) and the match is computed locally — the full hash, the password, and the entry it belongs to never leave. A password already in the bundled common-password list is answered offline and produces no request at all; repeat lookups of the same prefix are cached in memory for an hour.

What k-anonymity does not hide, stated plainly:

  • the five-character prefix (20 bits, one bucket in ~1M) goes to Cloudflare, together with your IP;
  • an observer who can guess candidate passwords can compute each candidate's prefix and see whether you queried that bucket — that is the known limit of this protocol, not something the header fixes;
  • Add-Padding only removes the response-size signal (without it, a bucket's response length correlates with how common the password is).

To keep even the prefix on your machine, pass online: false — the check then uses only the offline list, which is far smaller, so treat it as a screening aid rather than a verdict.

Strength indicator

Each entry row shows a three-star indicator after its title — ☆☆☆ = 0 score, ★★★ = full marks — in six half-star steps:

strength indicator: the seven states

A half star is a real half-filled star, not a ½ next to it: the coloured copy of the same three glyphs is clipped to a percentage, and ★/☆ share an advance width so the clip lands exactly on a star boundary.

| score | filled stars | band | |---|---|---| | 0–8 | 0 | weak | | 9–24 | ½ | weak | | 25–41 | 1 | weak | | 42–58 | 1½ | fair | | 59–74 | 2 | fair | | 75–91 | 2½ | strong | | 92–100 | 3 | very strong |

Hovering it shows the exact score and verdict. The score is computed host-side (the list never receives the secret) and covers the entry's password or card PIN; machine-generated API keys and private keys are deliberately not scored, so every key does not sit at full marks.

Environment export

Entries tagged env can be materialised as KEY=VALUE lines (vault_env, vault_export_env), and their names follow the field names every toolchain expects:

| Entry | Exported key | |---|---| | title DASHSCOPE, apiKey | DASHSCOPE_API_KEY | | title DASHSCOPE, prefix: APP_ | APP_DASHSCOPE_API_KEY | | envKey: "TAVILY_TOKEN", accessToken | TAVILY_TOKEN | | …the same entry's refreshToken / fields.scope | TAVILY_TOKEN_REFRESH_TOKEN / TAVILY_TOKEN_SCOPE |

  • The field suffix is vendor-standard: apiKey → API_KEY, accessToken → ACCESS_TOKEN, refreshToken → REFRESH_TOKEN, privateKey → PRIVATE_KEY, password → PASSWORD, cardNumber → CARD_NUMBER.

  • Set an entry's env-var names (optional field envKeys, also via vault_add/vault_update, or comma-separated in the editor) when the derived names are not what your scripts expect. They are positional and used verbatim (no prefix, no title):

    | envKeys | exported | |---|---| | ["DASHSCOPE_API_KEY"] | DASHSCOPE_API_KEY=apiKey | | ["GOOGLE_ACCESS_TOKEN", "GOOGLE_REFRESH_TOKEN"] | ...=accessToken, ...=refreshToken | | ["MY_KEY"] on an entry with 3 secrets | MY_KEY, MY_KEY_REFRESH_TOKEN, MY_KEY_SCOPE |

    Each name must be a POSIX identifier ([A-Za-z_][A-Za-z0-9_]*), at most 8, and no duplicates. envKey (singular string) is still accepted as a one-element shorthand and is folded into envKeys on write.

  • Custom fields are exported too, as <BASE>_<FIELD>.

Upgrading: keys changed from DASHSCOPE_APIKEY to DASHSCOPE_API_KEY in 1.10.64 to match vendor conventions. Set envKey on an entry to pin an exact name if a script depends on the old spelling.

Tool profiles

The plugin ships 110+ model tools, but registers only 11 core tools by default (vault_list/search/get/add/update/delete/fill/clipboard/totp/generate_password/strength) to keep the model's tool catalog small and cheap.

Switch profiles in Settings → Credentials → Permissions → Model tools — it applies instantly (no restart) and is persisted per vault:

| Profile | Registers | |---|---| | Basic (default) | core only — everyday use | | Standard | + management: favourites, tags, icons, expiry/rotation, health, duplicates/merge, attachments, templates, env masks | | Full | everything, incl. bulk import/export, browser sessions, backups and vault-file operations | | Custom… | core + any of: Management / Import-export / Browser sessions / Backups & files |

tools: basic|standard|full|custom can also be set in the plugin config; the UI choice wins and is stored in <vault dir>/tools.json (the process-wide tool catalog is not per vault).

Configuration

[!WARNING] Security: never put the master password in plaintext A profile patch (cordis.patch.yml / cordis.yml) is a config file that is easy to commit to git, screenshot, or capture in logs — so a plaintext masterPassword: row leaks the key to your whole encrypted vault. Always use masterPasswordEnv and export the real password into the process/shell environment instead (e.g. export DSH_VAULT_PASSWORD='…'). See the community-plugin-audit handbook for the evidence-first checklist for credential-handling plugins.

# ✗ DON'T — plaintext master password in a config file
- id: vault
  name: dsh-vault
  config:
    masterPassword: 'my-secret'
# ✓ DO — reference an environment variable; keep the file itself 0600
- id: vault
  name: dsh-vault
  config:
    masterPasswordEnv: DSH_VAULT_PASSWORD

| 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 [email protected]: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 496 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, 496)
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 496 tests pass (crypto / TOTP / password generation / store CRUD / gateway / integration).

Browser checks run against a real dsh web instead of vitest, and every one of them refuses to operate on the default vault:

node tests/e2e/theme-check.mjs      # dual-theme tokens, ring track, attach row
node tests/e2e/polish-check.mjs     # empty-state copy, relative-age hover hints
node tests/e2e/contrast-audit.mjs   # WCAG sweep over all eight tabs, both themes

See tests/e2e/README.md for the safety rules and for the two traps that make a contrast audit lie (serialised color(srgb …) values, and opacity counting as part of the colour).

Keeping credentials out of public content

Everything in this repository is public — the npm tarball, the GitHub releases, the issue and discussion posts. Before publishing, pnpm scan:secrets checks the working tree, the tracked files and the commit messages for anything that looks like a credential, and pnpm scan:public additionally checks every release and comment already posted. The local half also runs in pnpm test, so a committed secret fails CI rather than shipping.

Theming

The UI reads the host's design tokens instead of hard-coded colours. A local semantic layer maps onto the real --dsw-alias-* namespace, with literal fallbacks so the page still renders outside DeepSeek Harness:

--v-text: var(--dsw-alias-label-primary, #1f2328);
--v-border: var(--dsw-alias-border-l2, #d9d9d9);
--v-success-text: color-mix(in srgb, var(--v-success) 55%, var(--v-text));

Because the aliases flip with body[data-ds-dark-theme], light and dark follow the host automatically — there is no second stylesheet and no theme prop.

Two rules the layer exists to enforce:

  • Never use state-*-primary as body text. Those tokens are tuned for fills and icons: on white they measure 2.28:1 (success) and 2.15:1 (warn), far below WCAG AA. The --v-*-text variants mix them towards the theme's own text colour, so one declaration darkens on light and lightens on dark (light 5.8/5.6/7.5:1, dark 12+/11+/6.5:1).
  • Never dim text with opacity. opacity: .8 turns a 5.8:1 colour into 3.9:1. Express "secondary" with a token (--v-text-2), not with transparency.

tests/e2e/contrast-audit.mjs verifies both across every tab in both themes.

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 at lib/client.js)
  • prepare script → self-contained build on git install (tsc host + tsdown client)
  • No runtime dependencies: what the host provides arrives through peerDependencies, so there are no duplicate instances. playwright-core — needed only by browser login sessions, and lazily required — is an optional peer, so a normal install does not download it (~13 MB). Install it into your profile when you want those tools: cd ~/.dsh/profiles/web && npm i playwright-core

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_get reads; vault_search/vault_update outputs never contain passwords, keys, or tokens. Secrets returned by vault_get enter 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.

Listed in

Submissions pending maintainer review: Anil-matcha/awesome-dsh-plugin #127 · 0xsline/awesome-deepseek-harness #563 · dsh-handbook #65