Static capability disclosure for DeepSeek Harness plugins: a Settings page and CLI that list installed plugins' capabilities, literal destinations, install scripts, and injection shapes (prompt registrations, shipped skill text, bundle patch), each with file:line evidence. Code-determined, reproducible, zero-token. Disclosure only — not a security verdict, never a safety claim.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-trust-check
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:liuwenji007/dsh-trust-check
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
Static capability disclosure for DeepSeek Harness plugins: permissions, injections, source, and install scripts — code-judged, reproducible, zero tokens.
Not an antivirus, and it makes no safety promises. It proves only what is provable: what a plugin really touches, what it injects, and whether its source can be checked — with evidence (file + line + snippet) for every claim, so you can verify it yourself.
This project produces facts, not "safe / trusted" verdicts, and it does not endorse plugins; filtering policy belongs to integrators and users. See Positioning and boundaries (Chinese).
Install
The settings UI needs dsh ≥ 0.1.0-rc.8 (recommend 0.1.1-rc.2). The Web UI depends on @deepseek-ai/dsh-client-store in the host module table. The market will not block a mismatched install; on an older host the settings page fails to load (dsh-client-store missed the module table).
The CLI does not need a DSH host — npx dsh-trust-check still works on older dsh.
Check the host first (settings UI only):
dsh --version
# if too old: npm i -g @deepseek-ai/dsh@latest
dsh plugin --profile web add dsh-trust-check
Restart dsh web, then open Settings → Plugin Trust. Already installed: update from Plugin Market, or dsh plugin --profile web add dsh-trust-check@latest, then restart.
A standalone CLI is also provided, independent of the DSH host:
npx dsh-trust-check # audit the default profile `web`
npx dsh-trust-check --profile work # audit another profile
npx dsh-trust-check --json # machine-readable output
# Audit any extracted package directory (no profile, no DSH)
npx dsh-trust-check --dir ./path/to/plugin
npx dsh-trust-check --dir ./pkg --spec npm:foo@1.0.0 --json
# Gate a script / CI step on the exit code (optional)
npx dsh-trust-check --dir ./pkg --exit-code
--dir and --profile are mutually exclusive. Both modes emit the same AuditResponse shape for --json: { schemaVersion, profile, dir?, generatedAt, plugins, errors }; in single-directory mode profile is an empty string and dir is the absolute path. See docs/audit-schema.md and docs/INTEGRATION.md.
The exit code is always 0 by default. With --exit-code: 0 nothing detected (in profile mode, also acknowledged), 1 review, 2 red line, 3 scan failed; profile mode reports the worst plugin. 0 only means this static pass found nothing — not that the plugin is safe. Usage errors (unknown flag, missing value for --dir etc.) print to stderr and exit 1.
| Settings → Plugin Trust | CLI --dir --json |
|---|---|
![]() |
![]() |
Troubleshooting
| If | Then |
|---|---|
Settings: dsh-client-store missed the module table |
Host too old: upgrade dsh ≥ 0.1.0-rc.8, restart Web; use the CLI above for audits in the meantime |
| No “Plugin Trust” in Settings | Confirm add + restart; or the client failed to load on an old host |
How to read the report
The settings UI and CLI use a decision-first layout. Reading order:
- Decision: badge + action line + "why be careful" (up to 3 bullets)
- Scan: capability chips → injection summary (collapsed) → source
- Evidence: grouped by capability, collapsed by default
| Verdict | Meaning | When |
|---|---|---|
| Red line(s) | Hard red line hit; stop by default — or confirm risk to keep using | redLines non-empty, current fingerprint not acknowledged |
| Risk accepted | You confirmed the current red-line risk | Has red lines, trust-ack.json matches this scan |
| Review | No hard red lines, but privileged capabilities or patch changes | Has capabilities or override/disable, no red lines |
| As expected | You acknowledged the current capability/shape fingerprint | trust-ack.json matches this scan (no red lines) |
| Nothing detected | No red lines or privileged capabilities — not a safety guarantee | Everything else |
score / summary stay in JSON for sorting and integrators; the settings UI does not show them. Injected token estimates are labeled as cost hints only.
Shape layer (code-judged)
Besides capability chips, the report extracts three kinds of literal facts from source:
- Literal destinations: URL / host / IP. Same-origin HTTP routes (e.g.
/dsh-market/check) do not count as destinations. Each row is labelled with how that line uses it (request argument, comparison only, XML namespace, link, assigned, usage unknown) and afile:line. The label is for reading; it does not change capabilities, red lines, or the score. - Workspace path escapes: absolute paths, home directory, traversal, …
- Secret touches: paths, sensitive env names.
These mean "we saw this string in source", not "this address/path is safe"; runtime-built URLs are invisible (a template URL still reports its host when ${…} comes after the host or directly follows a complete dotted host, as in `https://api.github.com${path}`; ${…} before the host is complete hides it).
Allowlist: common host / registry domains (GitHub, npm, npmmirror, Tencent Cloud mirrors, …), the curated DSH catalog and GitHub proxies, and common model-vendor APIs (DeepSeek, OpenAI, Anthropic, Google Gemini). Allowlisted entries are collapsed by default with a short note; plaintext HTTP is never downgraded by the allowlist.
Noise reduction and truncation:
- Skips network-aligned CIDR table rows (e.g.
["10.0.0.0", 8],inRange(a, "10.0.0.0", "10.255.255.255")); host routes like["8.8.8.8", 32]still count as literal IPs. APRIVATE_RANGES/CIDRtoken on the line, or two IPs on one call, does not wipe the line. - Skips RFC 5737 documentation ranges (
192.0.2.0/24,198.51.100.0/24,203.0.113.0/24), and0.0.0.0/255.255.255.255(bind / broadcast, not unicast outbound). - Skips placeholder bases like
http://local/http://dsh.invalid, RFC 2606.example/.invalid/.test, and shell switches (/c). - Comments are blanked before the scan, so an example URL in a JSDoc block is not a destination (bundlers usually keep those comments).
- Namespace identifiers such as
xmlns="http://www.w3.org/2000/svg"and pdf.js'shttp://www.xfa.org/schema/…/http://ns.adobe.com/xdp/are excluded by exact host — an attacker cannot register those domains, so the exemption cannot be borrowed. Only literals with a path qualify: a bare"http://www.w3.org"becomes another host once anything is appended, so it is still a destination. - A few whole URLs that bundled format libraries use as identifiers, not requests (the Apple plist DTD
http://www.apple.com/DTDs/PropertyList-1.0.dtd, the ID3 ownerhttp://musicbrainz.org), are excluded by exact literal. Any other path or subdomain on those hosts, or the literal followed by+/.concat(, is still a destination. The barehttp://musicbrainz.orgis only exempt as an==/===/!=/!==operand. - When findings exceed the cap, the riskiest are kept: plaintext HTTP and literal IPs cannot be crowded out by harmless addresses.
- Secret paths require a path-only quoted string (no whitespace):
"~/.ssh/config","/Users/x/.ssh/config",'.ssh/config',"~/.aws/credentials", or path forms like/id_rsa/~/.netrc. UI prose ("Uses … ~/.ssh/config when empty"), deny-list regexes,startsWith('id_rsa'), and a bare'.ssh'are not credential access.
Mark as expected
After you confirm capabilities match why you installed the plugin, the fingerprint is stored in ~/.dsh/profiles/<profile>/trust-ack.json. An upgrade that changes capabilities, destinations, path escapes, secrets, red lines, or skill text (by content hash, not byte length) must be confirmed again. The confirmation request sends the report's ackFingerprint; a mismatch returns 409 and asks you to review the refreshed report. Older acknowledgements without a digest no longer match. Accepting a red line still requires its own "confirm risk" action, and only after the fingerprint matches.
AI explain: optional button; uses your DSH-configured model to explain the report summary only, does not change the verdict; unavailable when no model is configured.
Red lines (default block)
- declares install / postinstall / preinstall scripts (not
prepare:prepareruns on pack/git install only — a score deduction, not a red line); cordis.patch.ymloverrides/disables an@deepseek-ai/*core bundle (matched byidorname);- reads credential/secret values (
credentials.resolve/read/readRecord/get*, including seam aliases and renamed destructures; keytar/keychaingetPasswordetc. includingimport * as/default as; or a secret path on the same line as a file read) and has network access — holding a handle only (const c = ctx.credentials,ctx.get('credentials')) or metadata (describe) is a capability chip, not a red line; - plaintext
http://to a literal host that is neither localhost nor an RFC 1918 private IP and has network; - literal IP outbound that is not loopback, documentation, bind/broadcast, or RFC 1918 private (
10/8,172.16/12,192.168/16) and has network. Private IPs are still listed as destinations and tagged private — a remote attacker cannot receive traffic sent to them, so they do not make a red line.169.254/16(including the cloud metadata endpoint) is not exempt and still red-lines.
Red lines cap the numeric score at 49 (avoiding "100 + high risk"). The verdict follows redLines, not the score: a low score (e.g. 9) can come from shell + network + unpinned spec stacking and should show review, not red line(s). JSON band may still be red (score below 50), but UI/CLI use verdict() — do not mix them.
What it audits
| Dimension | Reads | Judgement |
|---|---|---|
| Capabilities | package.json dependency scopes + static scan of lib/, dist/, bin/, scripts/, skill dirs, and main / exports / bin / string browser entry files. When files parses exactly, dev-only files npm would not publish are not counted |
shell / file read / file write / network / credentials / sub-agents / LLM calls / env reads |
| Injections | cordis.patch.yml + systemPrompt / ctx.skills.register / system-prompt/assemble + skill text |
who it overrides/disables (id or name), what it injects |
| Cost | skill text + system-prompt inline literal bytes | estimated injected tokens per request (bytes / 4, estimate only) |
| Source | package.json repository (falls back to the git install source) + install spec |
pinned version / pinned commit |
| Update risk | install scripts (install/postinstall/preinstall; prepare is deduction-only) |
whether scripts are declared to run at install time |
For integrators (plugin catalogs, markets, CI)
How to wire a pre-install gate: docs/INTEGRATION.md.
JSON field contract: docs/audit-schema.md (schemaVersion, stable gate fields, the wording contract for capabilities values and red-line templates with suggested Chinese, volatile fields).
Release changes: CHANGELOG.md lists "Affects catalog results" first on every release — the section to read before bumping a pin.
Real-world integration: awesome-dsh-plugin runs this tool at catalog build time; its adapter is scripts/lib/capabilities.mjs (third-party code, maintained there).
Stable exports for pre-install confirmation or CI gates:
import { auditPlugin, collectPlugin, verdict } from 'dsh-trust-check'
const report = auditPlugin(collectPlugin(extractedDir, spec))
// What the three states mean (whether to block or prompt is the integrator's call):
// verdict(report) === 'red' → high-confidence risk signal; suggest blocking by default, user may confirm
// verdict(report) === 'review' → privileged capabilities; suggest showing the capability list
// verdict(report) === 'clear' → nothing detected in this static pass; not a safety claim
CLI equivalent (market can spawn without DSH):
npx dsh-trust-check --dir "$EXTRACTED_DIR" --spec "$INSTALL_SPEC" --json
Parse --json uniformly: plugins[0] for single --dir, or the full plugins array for profile mode; non-empty errors means the directory could not be read — treat as scan failure, not clear. An empty / corrupt extract (no readable package.json and no scannable sources) lands in errors (fail closed). Top-level schemaVersion is currently 2 — it bumps on a breaking shape change or when a field becomes a stable contract, not when detection rules change. Schema 2 adds facts[], whose id is a stable filter key; no schema 1 field is removed or renamed, and the capability values and red-line templates are unchanged. See the two docs above; in-repo Path A smoke demo: scripts/market-gate-demo.mjs.
Out of scope for this release: remote tarball download (market's job). Workflow: extract to a temp dir, then --dir.
Principles
- Code-judged, not LLM-scored: every judgement is code, zero tokens, reproducible.
- Proves presence, never absence: static analysis only claims "detected X"; it never claims "guaranteed no Y".
- Evidence you can re-check: every capability hit carries
file:lineand a snippet. - Hot-swappable seam table: detection rules are a data table (
src/core/seams.ts); DSH API changes mean editing the table, not the engine.
Known limits
- Post-install checkup: profile mode audits plugins already installed; install/postinstall/prepare may have run before your first scan.
--dircan scan an extracted tree before install (but install scripts may still run during market extract/install). - Static scans miss/false-positive (runtime-loaded capabilities are invisible; dynamic
import('node:' + …), string concatenation, and obfuscatedeval/Functioncan still bypass the rule table). - Does not expand the dependency tree: the directory walk skips
node_modules, and a bareimport 'lodash'is not followed. A relative path that lands inside this package'snode_modulesis scanned. - Credential-value reads cover: seam service (
ctx.get('credentials'), includingawait/ optionalget?.; receiversctx/this.ctx/*Ctx), property access (ctx.credentials), destructuring off context (includingcredentials: creds), rebinds of those aliases (includingb = a), and keytar/keychain password reads including default /import * as/import { default as … }/requirerenames. Still missed on purpose: destructuring down to a bare function name (const { resolve } = ctx.credentialsthenresolve(…)) — same shape as Promise executors and would red-line plugins likedsh-pocket/agent-teamswithout scope tracking; a secret path held in a variable thenreadFileSync(p); receivers named neitherctxnor*Ctx(e.g.context/Context). new URLbase args are not destinations, soconst u = new URL('/x', 'http://evil'); fetch(u.href)has no plaintext-http red line — the known cost of that exemption; do not widen it.- A complete relative-path literal (
fetch('/api'),fetch('./x')) and ablob:/data:literal are notnetwork— the former is same-origin, the latter is scheme fetch and never enters HTTP. A quoted${is literal path text. In a template, an interpolation after/plus a non-slash character, or after.//../, is same-origin too (fetch(`/dsh-xu/${name}`)).fetch(`/${name}`),fetch(`${url}`), variable arguments, and concatenation still count, as does a literal that becomes//hostafter\/\n/\u/ octal (\057) folding. The chip is labelled same-origin or outbound, but a missing outbound literal is not a proof of no outbound access, and the score is unchanged. - A
link:or source-directory scan drops files npm would not publish whenpackage.jsonfilesparses exactly (files imported by shipped code, named by an install script, or declared as an entry are still scanned).*means every path, anddir/*means that whole directory;src*does not include files undersrc/. Braces, character classes, extglob, and backslashes are not filtered. "Browser / server / command line / suspected dependency" on a row comes from which entry reaches the file and from the sourcemap; it is for reading and does not change the score. An extracted registry tarball already contains only published files. - Injected tokens are a byte / 4 estimate, not exact billing.
link:/file:installs can't infer source from the spec; if the package.json lacksrepository, it shows "no repository declared".- The
repositoryfield is self-declared; it is not cross-checked against the npm package name, and a non-http(s)scheme is never rendered as a clickable link.
Development
pnpm install
pnpm build # tsdown: node half → lib/index.js, client half → lib/client.js
pnpm test # vitest, covers the core engine
pnpm typecheck # tsc --noEmit
Rule-table, skip-rule, and allowlist contributions: CONTRIBUTING.md. Widening a skip or allowlist is reviewed harder than adding a rule — both can weaken detection; plaintext HTTP is never downgraded by the allowlist. False-positive fixes must include a fail-open probe. The attack classes rules are held against, and the three limits of static analysis, are in THREAT-MODEL.md.
Roadmap
- Now: installed-plugin audit + CLI
--dir+ Web dimension-first report; ongoing scan-coverage hardening - Integration: awesome-dsh-plugin scans at catalog build time; dsh-market and the catalog site only display the facts (maintained there, replaceable)
- v2: structured
facts[](schemaVersion: 2) since 0.2.0; next are a version-to-version drift diff and caller-supplied registry metadata - Later: keep up with catalog pin bumps and false-positive reports; drift assertions in CI
- Conditional: named community human review — separate repo, bound to a version, revocable
Full write-up: docs/POSITIONING.md (Chinese).
License
MIT
Links
More in this category
toby-bridges/api-relay-audit★ 876
Runs local security audits of AI API relays and LLM proxies from DeepSeek Harness, producing Markdown reports for prompt injection, model substitution signals, tool-call rewriting, error leakage, stream integrity, and profile-gated Web3 risks.
SeaOf0/dsh-redteam-model★ 685
Authorized-security DSH collection: nine work modes (redteam coordinator, pentest, code audit, binary analysis, attack-defense, AV evasion, incident response, cloud security, CTF solving) and fifteen runtime plugins, managed from a settings page with one-click deploy, install, update and uninstall.
howmp/dsh-pentest★ 607
Authorized pentest mode for DeepSeek Harness — exploration chain, assets and findings with a Web view.
PerryLink/dsh-auto-review★ 234
Second-model auto-review on the approval answerer chain: a read-only reviewer subagent returns structured allow/deny verdicts with reasons, fail-closed by default.
NanmiCoder/dsh-auto-mode★ 164
Adds an Auto permission preset between Workspace Write and Full access: routine work stays in the official workspace-write sandbox while the current session model reviews escalation and destructive calls, granting one exact wider access once, asking when the intent is ambiguous, and denying critical paths.
ADWMC/helm-d#helmd★ 151
Single-bundle reverse-engineering and pentest security plugin: first-turn tool narrowing, domain routing, and 33 tools covering APK, web, native binary, protocol, malware and LLM samples — with unpacking, license-bypass and anti-analysis case playbooks, an H-CoT evaluation engine (semantic routing, /hcot command), a web workbench with a ledger-driven tool shelf, an on-disk case workflow (auto-persisted evidence chain, E-numbered validated findings, post-compaction resume), GitHub-based external tool discovery, and 361 on-demand reference docs.


Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.