メインコンテンツへスキップ
C

dsh-dlp

charlotten7/dsh-dlp

Data-loss-prevention plugin for DeepSeek Harness: a non-configurable tool guard floor, tool-result redaction, and fail-closed telemetry redaction

インストール

dsh plugin --profile web add github:charlotten7/dsh-dlp

README

dsh-dlp

Data-loss prevention for DeepSeek Harness, built as an out-of-repo plugin.

📖 Full documentation

What it does

  1. Denies credential-file access and secrets bound for the network — unconditionally, from ctx.tools.guard(), testing path-typed arguments against a table of credential stores and following symlinks first.
  2. Redacts secrets out of tool results before the model reads them and before the session log records them, withholding a result it cannot clean.
  3. Redacts secrets out of exported telemetry, closing a hole where DSH_TELEMETRY_MODE=FULL ships message text, tool arguments, results and workspace paths in the clear.
  4. Strips invisible characters that carry hidden instructions — the Tags block, bidi overrides, runs of variation selectors — and strips terminal control sequences from the audit lane so a tool result cannot forge its own audit record.
  5. Neutralises remote markdown images in assistant output and detects a tool call another plugin rewrote after the session log recorded it.
  6. Asks before the agent writes a file that changes future behaviour — agent settings and hooks, CLAUDE.md, .claude/rules/** and the other agent rules directories, prompt templates, .vscode/tasks.json, .mcp.json, git hooks, CI workflows, shell startup files, pnpm-workspace.yaml — and before it writes a *_BASE_URL that would redirect a provider credential.
  7. Asks before a call switches off its own confirmationnon_interactive: true, approval_mode: auto, an apply whose approval is still pending. Both ask tiers are prompts rather than controls: they live at tools/pre-execute and can be neutralised.
  8. Writes an audit record for every decision — rule id, rule version, offsets, keyed hash. Never the secret, never the path or command that matched. dsh-dlp report reads it back.

What this is not

This is not a containment boundary. The plugin runs in-process, at the agent's own uid. Anything the agent can execute — a bash command, a run_code program, a mounted MCP server — can read every file the guard denies and open its own sockets without the plugin seeing anything. It closes the path where the model asks for credential material through a tool. It does not stop code that is already running. If you need containment, that is the sandbox, landlock-run, filesystem permissions and egress firewalling.

Three limits worth knowing before you rely on it:

  • Only the guard floor is unconditional. Every other seam can be neutralised by a listener registered ahead of ours. ctx.tools.guard() is order-independent only because it has no allow arm.
  • The shell-command arm is advisory pattern-matching. It catches an unobfuscated cat ~/.ssh/id_rsa and nothing that tries — one glob character, a $(printf …) reassembly or python3 -c all defeat it, each verified. Do not count this arm as a control.
  • Detection is pattern-based. No entropy rule (measured, not assumed: at a false-positive-free threshold the miss rate is 100% below 22 characters). Encoded forms pass. A homoglyph defeats every rule in this package.

The full list of limits →

Install

A profile carrying only @deepseek-ai/dsh-base has no agent loop, so add a runnable bundle alongside it or there is nothing for this plugin to guard:

dsh plugin --profile <name> add @deepseek-ai/dsh-headless@0.1.0-rc.6
dsh plugin --profile <name> add dsh-dlp
dsh --profile <name> --dump-config      # the dsh-dlp row should appear

Any harness from 0.1.0-rc.6 onwards in the 0.1.x line works: the peer ranges accept it and CI runs the end-to-end suite against every published rc in that range.

Pin @deepseek-ai/dsh-headless explicitly — its npm latest tag still points at 0.0.1-rc.1. The package ships a cordis.patch.yml bundle layer, so listing it in dsh.profile.bundles mounts it with working defaults.

Install from the registry or a packed tarball, not from a git spec: lib/ is a build output git does not carry and no prepare script rebuilds it, so a git-spec row mounts and then fails to load.

Configure

- id: dsh-dlp
  config:
    auditLog: /var/log/dsh-dlp.audit.jsonl
    redactionKeyFile: /var/lib/dsh/dsh-dlp.redaction-key
    policyFile: ./.dsh-dlp.yml        # optional, lowest trust
    breadthTier: true
    resultRedaction: true
    telemetryRedaction: true
    configWriteAsk: true
    approvalSuppressionAsk: true

redactionKeyFile is created on first mount with 32 random bytes at mode 0600. Keep it out of version control — it is what makes a placeholder's hash keyed rather than a bare digest anyone holding a candidate secret could confirm.

The guard floor has no configuration. Credential-path denial and secret-argument denial are security invariants, not deployment-varying tunables. A repo-local policyFile is the lowest trust rank and may only tighten: add deny patterns, add egress tool names, raise a severity, switch a pass on. Any downgrade makes the whole file invalid.

Configuration reference → · What gets denied → · Redaction and detection →

Reading the audit log

dsh-dlp report                       # everything in the audit sink
dsh-dlp report --since 24h
dsh-dlp report --kind guard-deny

Every record carries a rule id, rule version, span offsets and a keyed hash — never the matched value.

Audit record format →

Mitigations for defects in the harness itself

Three registrations work around defects in DeepSeek Harness rather than in your configuration: remote markdown images in assistant output, a tool call rewritten between tools/pre-execute and the guard, and a telemetry redactor that cannot run under the shipped default. None of them closes its channel and an upstream fix is better in all three cases.

What each one does and does not close →

Development

nvm use 22           # Node ^22.19.0 || >=24, and pnpm 11
pnpm install
pnpm run typecheck
pnpm run test:coverage
pnpm run test:e2e    # boots a real dsh against a mock model; no API key

Coverage is gated at 100% per file: this is a security control, so an untested branch in a deny path is an unproven deny path.

Design decisions and their rationale live in ADR.md. Security policy is in SECURITY.md.

License

MIT

関連プラグイン