Blocks agents from reading or writing sensitive files (.env, credentials, key material), masks leaked secret-shaped values in tool results, keeps an audit journal, and exposes safe sg_* inspection tools that never print raw values.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:JohnXu22786/secret-guard
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. Only install sources you trust, and pin a commit (github:owner/repo#sha).
README
secret-guard
A security plugin for the DeepSeek Harness (dsh): it intercepts reads and writes of sensitive files (.env, credentials, key material, etc.) by the agent's file tools before they execute, preventing API keys and other secrets from leaking into the conversation context; it also applies a content-masking fallback on tool results so that even content that slips past the interception gets scrubbed.
- Zero build: loads as pure TypeScript source (dsh loads
.tsvia Node's native strip-only type stripping, so the source must not use syntax unsupported by strip, such as parameter properties — this repo complies and ships annpm run smoke:stripsmoke check); self-contained, with only@deepseek-ai/dsh-tools(tool definitions) andschemastery(config validation) as runtime dependencies. - Interception layer:
tools/pre-executewaterfall event (short-circuits before the tool body runs). - Fallback layer:
tools/post-executewaterfall event (shape-recognition masking of result content). - Companion
sg_*safety inspection tools: they only return key names, line numbers, shapes, booleans, and HMAC fingerprints — they never return raw values. - Audit journal: JSONL append-only, rotated by size; rule files hot-reload (automatic polling + manual
sg_reload).
Directory Structure
secret-guard/
package.json # dsh.bundle.patch declaration; main points at the source
cordis.patch.yml # bundle patch layer (plugin line: id / name / config)
src/
index.ts # plugin entry: name / inject / Config / apply
config.ts # config schema (schemastery) + validation and defaults
policy.ts # rule engine: path normalization, glob compilation, default rule table
gate.ts # tools/pre-execute interception listener
scrub.ts # tools/post-execute content-masking listener
inspect.ts # dotenv parsing, value shape classification, sg_* safety tools
fingerprint.ts # HMAC-SHA256 seal key and fingerprints
journal.ts # JSONL audit journal + rotation
watch.ts # rule-file hot-reload poller
tests/ # node:test + tsx, runs without compilation
README.md
LICENSE
Installing in DSH
dsh plugin --profile demo add github:JohnXu22786/secret-guard
The default configuration takes effect after installation. To remove:
dsh plugin --profile demo remove dsh-secret-guard
Installation and Integration (how dsh loads it)
The plugin follows dsh's Cordis plugin conventions: an ESM module (entry src/index.ts) exporting name / inject / Config / apply(ctx, config), inserted into the plugin tree by cordis.patch.yml as a bundle layer. All registered side effects (event listeners, tool registration, file polling) are reversibly cleaned up by the function returned from apply and by the Cordis context.
# install from this directory into the web profile (equivalent to pnpm link + bundle-layer load)
dsh plugin --profile web add .
# or the headless profile
dsh plugin --profile headless add .
The default configuration is active right after installation. Inspect the plugin tree:
dsh --profile web --dump-config | grep -A 6 secret-guard
Local Development (without installing)
When the plugin is not installed in a profile, the package name in the patch line cannot be resolved from the profile directory, so you need an absolute-path overlay pointing at the source entry. Note that on Windows, Node's ESM does not recognize drive-letter paths (D:/… is treated as a protocol) and requires a file:/// prefix:
# dev-overlay.yml
- insert:
- id: secret-guard-dev
name: 'file:///D:/path/to/secret-guard/src/index.ts'
config:
maskResults: true
dsh --profile headless --patch D:/path/to/secret-guard/dev-overlay.yml "list the current directory"
Requirements
- dsh ≥ 0.1.0-rc.6 (the
@deepseek-ai/dsh-toolstypings match the 0.1.0-rc.6 interface surface) - Node.js ≥ 22.19 (or ≥ 24)
- peer dependencies:
@deepseek-ai/cordis,@deepseek-ai/dsh-tools,@deepseek-ai/dsh-llm(types only),schemastery(must be provided by the host profile;dsh plugin addinstalls peers automatically)
Plugin Interface at a Glance
| Interface | Description |
|---|---|
| Manifest | dsh.bundle.patch in package.json → cordis.patch.yml |
| Entry | src/index.ts: name='secret-guard', inject=['tools'], apply(ctx, config) |
| Interception event | consumes tools/pre-execute (returns {kind:'deny', reason} on refusal, short-circuiting the waterfall) |
| Masking event | consumes tools/post-execute (returns an accept decision with the replacement content) |
| Tools | registers sg_keys / sg_scan / sg_fingerprint / sg_probe / sg_status / sg_reload |
| Config | the config field of the plugin line (see below), validated by the schemastery Config |
Configuration
Configuration lives under the config key of the plugin line in cordis.patch.yml, or can be overridden via the profile's cordis.patch.yml patch:
- insert:
- id: secret-guard
name: 'dsh-secret-guard'
config:
# custom rules (evaluated before the default rules; first match wins)
rules:
- id: my-prod-creds
match: '**/prod-secrets.yml'
effect: block # block | block-read | block-write | allow
reason: 'production credentials, no access'
# allowlist (checked before any rules; also supports globs)
allow:
- 'tests/fixtures/.env'
- '**/sandbox.env'
# tools participating in interception (file-type tools whose args contain file_path / path; includes read_image by default)
gateTools: [read, write, edit, glob, grep, read_image]
# also intercept grep whose pattern hits sensitive keywords (env/credential/password…)
guardSearchPatterns: true
# content-masking fallback for results
maskResults: true
# seal key (HMAC): environment variable first, then a local file (auto-created, 0600)
sealKey:
env: SECRET_GUARD_SEAL_KEY
path: .secret-guard/seal.key
# audit journal
audit:
enabled: true
dir: .secret-guard/logs
maxBytes: 1048576 # rotate when a single file exceeds this
keep: 5 # number of rotation files to keep
# external rule file (JSON, hot-reloadable): { "rules": [...], "allow": [...] }
rulesFile: .secret-guard/rules.json
watchRules: true # automatic polling hot-reload (~400ms interval)
Relative paths (sealKey.path, audit.dir, rulesFile) resolve against dsh's startup working directory.
Rule Syntax and the Default Rule Table
Rule match uses a .gitignore-like glob:
- Patterns containing
/anchor the full path (**/.aws/credentialsmatches at any depth); a**/prefix can match zero directory levels; - Patterns without
/only match the file name (.envmatches.envin any directory); **spans directory segments,*matches anything within a segment,?matches a single character;- Matching is case-insensitive (safer for interception rules).
Evaluation order: allow allowlist → custom rules (in declaration order) → built-in default rules (first match stops). The allowlist behaves like allow in rules, but is always checked first.
Match target: both rules and the allowlist operate on normalized paths — forward slashes, drive letter and leading / stripped, .. collapsed (a/../b → b). So write patterns in relative form (e.g., tests/fixtures/.env), without drive letters (C:\…); allowlist entries written as absolute paths will silently fail.
Built-in default rules (the id is the rule field in the journal):
| id | Matches | Effect | Description |
|---|---|---|---|
guard-vault |
**/.secret-guard/** |
block | The plugin's own storage (seal key, audit journal) |
env-example ~ env-default |
.env.example / .env.sample / .env.template / .env.dist / .env.default |
allow | Safe example files |
env-file |
.env |
block | May contain real secrets |
env-variant |
.env.* |
block | Environment-specific secret files |
env-suffixed |
*.env |
block-read | Non-standard naming (e.g., api.env) |
aws-credentials |
**/.aws/credentials |
block | |
git-credentials |
**/.git-credentials |
block | |
netrc |
**/.netrc |
block | |
npmrc-auth |
**/.npmrc |
block-read | Contains registry tokens |
pypirc |
**/.pypirc |
block-read | Contains registry tokens |
credential-files |
*credential* |
block | Credential stores |
ssh-rsa / ssh-ed25519 / ssh-ecdsa / ssh-dsa |
id_rsa etc. |
block-read | SSH private keys (writes allowed, supporting key-generation flows) |
key-ext-* |
*.pem *.key *.ppk *.p12 *.pfx *.jks *.keystore *.kdbx |
block-read | Private key / keystore material |
Effect semantics: block intercepts both reads and writes; block-read only intercepts read-type tools (read/read_image/glob/grep), writes pass; block-write is the opposite; allow passes everything.
Safety Inspection Tools (sg_*)
These tools are intentionally allowed to read blocked files — that is their whole purpose — but they only return metadata, and no output path ever contains raw values.
| Tool | Purpose | Returns |
|---|---|---|
sg_keys |
List the keys of a dotenv file | key names, line numbers, empty-or-not, value shape labels |
sg_scan |
Value-shape scan | each key's shape (empty/bool/numeric/jwt/url/hex/base64/opaque) and length |
sg_fingerprint |
Deterministic fingerprint of a single key | first 16 hex chars of HMAC-SHA256 (stable under the same seal key) |
sg_probe |
Boolean questions about a single key | boolean results of is-set / is-empty / starts-with / ends-with / contains / matches (regex, executed in a separate worker thread with a timeout so pathological regexes cannot hang the host) / equals (constant-time comparison, no plaintext exchange) |
sg_status |
View the current policy | rule count, allowlist, gated tools, mask/audit toggles; the check parameter trial-classifies any path |
sg_reload |
Immediately re-read the external rule file | reload result (rule count / allowlist count / error message) |
Usage examples (agent's perspective):
sg_status { check: ".env" } # -> block (rule 'env-file')
sg_keys { file: ".env" } # lists only key names
sg_probe { file: ".env", key: "DB_PASSWORD", op: "equals", value: "candidate" } # true/false
sg_fingerprint { file: ".env", key: "DB_PASSWORD" } # 9f2c… stable fingerprint
Content-Masking Fallback (scrub)
Even if interception is bypassed (e.g., cat .env via a bash-type tool, MCP tools, or tools not listed in gateTools), tools/post-execute still runs shape recognition on result text, replacing suspected secrets with [redacted:<type>:<length>], and appends a summary line stating how much was scrubbed.
Capability boundary (important): masking is text-shape matching, not a security boundary. Transforming content before output (cat .env | base64, rev, splitting and rejoining lines, etc.) defeats all shape rules; likewise, symlinks and path aliases like .. are known limitations of string classification (see "Security Notes"). It only stops the most common path — the model reading secrets verbatim into context — and is not a substitute for the interception layer.
Recognized shapes (deliberately conservative, to avoid harming hashes, UUIDs, and ordinary long strings):
- PEM private key blocks (
-----BEGIN … PRIVATE KEY-----) - JWTs (
eyJ…three segments) - Bearer tokens (
Bearer <16+ chars>) - Known key prefixes:
ghp_/gho_/ghu_/ghs_/ghr_,sk-,AKIA…,xox[baprs]- - Connection strings with plaintext (
scheme://user:pass@host, requires a:password@) key=valueassignments (key name containing password/passwd/secret/token/api_key/access_key/client_secret/private_key/auth_key, value ≥ 12 chars and not a doc placeholder like<placeholder>/xxx/example)
Deliberately not masked: bare base64/hex long strings (git commit hashes, UUIDs, etc. have high false-positive rates); they are only handled when they appear in the contexts above.
Audit Journal
Every interception (block), masking (mask), rule reload (reload), and error (error) is written to audit.dir/events.jsonl (JSONL, append-only). When a file exceeds maxBytes it is automatically rotated to events.1.jsonl, events.2.jsonl, …, keeping at most keep files.
Journal contract: values are never recorded — entries only contain timestamp, event type, tool name, path, rule id, effect, shape counts, and message. The journal path itself is covered by the default rule guard-vault.
Rule Hot-Reload
Point rulesFile at a JSON file ({ "rules": [...], "allow": [...] }, same structure as the identically named config fields):
- With
watchRules: true, it polls every 400ms and, after a 300ms debounce on file change (mtime/size), auto-reloads; - Or call
sg_reloadat any time to reload immediately; - Successful and failed reloads are both written to the audit journal and shown on the console; a failed load never destroys the currently active rules (the old engine is kept).
Implementation note: polling is used instead of fs.watch because on Windows, deleting a watched directory leaks the event loop's exit eligibility (a platform defect), and rule files are often replaced by editors via rename; polling behaves consistently across platforms and leaks nothing (timers are unref'd).
Security Notes
- The seal key is the root key for HMAC fingerprints: never commit
.secret-guard/seal.key— add it to.gitignore; fingerprints change after moving machines or resetting the key (expected behavior). - Keep
sealKey.path/audit.dirunder.secret-guard/by default (the built-inguard-vaultrule protects them). If you move them elsewhere, configure equivalent interception rules yourself, or the agent may read the key file or audit journal. - The default rules are deliberately conservative (prefer over-blocking); when over-blocked, use the
allowallowlist for precise exemptions, or verify classification first withsg_status {check: …}. - This plugin does not migrate, encrypt, or move any secret files; it only prevents "reading secrets into the model context".
- Known limitations (design trade-offs): path classification is pure string matching — it does not resolve symlink targets, and
..collapsing only covers the string layer; reading sensitive files through execution-type tools likebashis not constrained by the interception layer (the masking fallback does its best to scrub output).sg_probe's boolean questions are a "value oracle" — in theory, multi-roundcontains/equalsqueries could reconstruct a value; it is designed for agents to verify values safely and should not be used in untrusted contexts.
Development and Testing
npm install # devDependencies only (tsx / typescript / cordis runtime / schemastery)
npm test # node --import tsx --test tests/*.test.ts (71 test cases, ~3s) + strip-only load smoke test
npm run typecheck # tsc --noEmit
Test coverage: rule engine (normalization/glob/default table/allowlist/custom rule precedence), dotenv parsing, fingerprints and seal keys, masking patterns and false-positive control, the interception decision matrix (including search tools and the keyword guard), audit rotation, and integration tests driving the waterfall events with a real Cordis context (including end-to-end hot reload).
License
This project is released under the MIT License.
Links
More in this category
superdesigndev/treg★ 425
Tool catalog for agents: search ~2,600 external endpoints (SEO and SERP, backlinks, social, people and company enrichment, ad libraries, scraping) by the task you want done, read each one's parameters and per-call price, then call it with the credential injected server-side. Ships the skill plus an MCP row that stays disabled until TREG_TOKEN is set.
Lum1104/dsh-browser★ 198
Chrome sidebar extension that lets DSH operate your browser directly, no vision capabilities required.
zhaoolee/notes★ 142
Export DSH conversations as Smartisan Notes-style PNGs, or create and update Markdown notes in a configured account-scoped workspace.
liustack/modsearch★ 111
Web search bridge for text-only agents: ask the web or X, get structured JSON evidence (search, fetch, citations).
taxueseek/argo★ 91
Search built for agents: multilingual coverage across web, academic, code, shopping, finance, news, and encyclopedias.
Vladimir-Human/ru-marketplace-mcp#dsh★ 63
Skills and optional MCP rows for ten Russian marketplaces: price comparison across Wildberries, Detsky Mir and Yandex Market, plus per-source search, product cards and reviews. The 13 skills load on install; both MCP rows stay disabled until RU_MARKETPLACE_MCP_DIR points at a local clone, which needs Python 3.12+ and uv.