dsh-tool-policy
Verifieddsh-tool-policy · v0.1.1 · MIT
Declarative tool-call permission control for DeepSeek Harness — allow/deny/ask rules over tools, paths and commands, fail-closed by default, with JSONL audit log.
Install
dsh plugin add dsh-tool-policy Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Readme
dsh-tool-policy
npm package name is
dsh-tool-policy(dsh-policyon the registry is a reserved placeholder); the harness plugin id staysdsh-policy.
Declarative tool-call permission control for DeepSeek Harness. Rules decide
allow / deny / ask before a tool runs (tools/pre-execute), outcomes
are recorded after (tools/post-execute), and every decision lands in a JSONL
audit log.
Design principles:
- Fail loud on config errors — invalid effects, broken regexes, and bad rule ids throw at plugin load, never silently disable your guardrails.
- Fail closed at runtime — evaluator errors deny (configurable), unmatched
calls fall back to
defaultDecision(defaultask). - No silent bypass — a call matching no rule is
ask, notallow.
Rule model
interface PolicyRule {
id: string // unique, referenced by the audit log
effect: 'allow' | 'deny' | 'ask'
priority?: number // default 0; higher wins; ties keep declaration order
tools?: string[] // glob patterns, e.g. ["bash", "write*", "search_*"]
commands?: string[] // regex sources, matched against extracted commands
paths?: string[] // glob patterns, matched against extracted paths
reason?: string // surfaced in decisions and the audit log
}
- Matching is conjunctive: a rule applies when its
toolsANDcommandsANDpathsconstraints all match (each dimension is an OR across its own patterns). Rules with no constraints never match (empty evidence is not a match) — usedefaultDecisionfor the catch-all. - First match wins after sorting by
priority(descending, stable). - Command extraction pulls string values under
command/cmd/shell/scriptkeys (nested objects and arrays included) and matches them as regexes. - Path extraction takes values under
path/file_path/… keys, any standalone absolute-path value (/…,C:\…), and absolute paths embedded in free-form strings; Windows separators are normalized before matching. - Globs:
*(no separators),**(any),?(one char); everything else is escaped. Paths are normalized to/first.
Install & enable
dsh plugin --profile web add dsh-tool-policy
The bundled row ships enabled: false (the harness keeps its own approval flow
until you opt in). A commented starter ruleset ships in
cordis.patch.yml — deny destructive commands, deny
sensitive paths, allow everyday dev commands, ask before file writes, allow
read-only tools.
- insert:
- id: dsh-policy
name: dsh-tool-policy
config:
enabled: true
defaultDecision: ask
failClosed: true
rules:
- id: deny-destructive-commands
effect: deny
priority: 100
tools: ["bash"]
commands: ["rm\\s+-rf\\s+/(?!tmp|var/folders)", "git\\s+push\\s+.*--force"]
reason: "Destructive command blocked"
- id: deny-sensitive-paths
effect: deny
priority: 90
paths: ["/etc/**", "/root/.ssh/**", "**/.env", "**/*.pem"]
- id: allow-dev-commands
effect: allow
priority: 50
tools: ["bash"]
commands: ["^(git|pnpm|npm|node|pytest|python|ls|cat|grep|rg|mkdir)\\b"]
A profile-level patch targeting this id replaces the whole row config (no deep merge) — restate every key you want to keep.
Configuration
| Key | Type | Default | Notes |
|---|---|---|---|
enabled |
boolean |
false |
No-op when false. |
rules |
PolicyRule[] |
[] |
See rule model above. Invalid rules throw at load (fail loud). |
defaultDecision |
'allow' | 'deny' | 'ask' |
'ask' |
Decision for calls matching no rule. |
failClosed |
boolean |
true |
Evaluator/internal errors → deny instead of defaultDecision. |
pathArgs |
string[] |
['path','file_path','filepath','filename','file','dir','directory','target'] |
Arg keys treated as path carriers. |
commandArgs |
string[] |
['command','cmd','shell','script'] |
Arg keys treated as command carriers. |
audit.enabled |
boolean |
true |
Write JSONL audit entries. |
audit.file |
string |
<cwd>/.dsh-policy/audit.jsonl |
Audit sink. |
Audit log
JSONL, one entry per line, phase-tagged:
{"ts":1760000000000,"phase":"decision","session_id":"s1","tool":"bash","args_hash":"3f9a…","decision":"deny","rule_id":"deny-destructive-commands","reason":"…","policy_version":"[email protected]"}
{"ts":1760000000500,"phase":"outcome","tool":"bash","args_hash":"3f9a…","outcome":"blocked","policy_version":"[email protected]"}
- Arg values are hashed (sha256, first 16 hex chars) — the log proves what was evaluated without storing raw payloads.
- Rotates at 5 MiB by renaming to
audit.jsonl.<timestamp>. - Audit write failures warn once and never throw into the tool path.
Relationship to other solutions
dsh-permission-rules(PerryLink) covers minimal allow/deny lists with an audit trail.dsh-tool-policyadds the three-dimensional rule model (tool × command × path), priorities with first-match-wins, regex command matching, embedded-path extraction, an explicitaskeffect wired into the approval chain, and fail-closed defaults — a superset aimed at unattended/CI profiles.- Claude Code's
PreToolUsehooks and Gemini CLI's policy engine solve the same problem in their own ecosystems;dsh-tool-policyports that posture to dsh.
Library use
The evaluator and helpers are exported for standalone use:
import { PolicyEvaluator, globToRegExp, extractCommands, extractPaths } from 'dsh-tool-policy'
const ev = new PolicyEvaluator(rules, { defaultDecision: 'ask', failClosed: true })
ev.evaluate({ tool: 'bash', args: { command: 'git push --force' }, sessionId: 's1' })
// => { decision: 'deny', ruleId: 'deny-destructive-commands', reason: '…' }
Compatibility
Targets @deepseek-ai/cordis ^4.0.1. See the repo-level
compatibility policy and the monthly CI job.
License
MIT