dsh-policy
Verified@amaster.ai/dsh-policy · v0.1.15 · MIT
Declarative tool-call policy for DeepSeek Harness: config-driven allow/deny/ask rules (tool name, args pattern, shell command prefix/regex, priority) on the tools/pre-execute gate — Gemini CLI-style policy files expressed as plain plugin config.
Install
dsh plugin add @amaster.ai/dsh-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.
Readme
@amaster.ai/dsh-policy

Declarative tool-call policy for DeepSeek Harness (dsh): config-driven allow / deny / ask rules on dsh's tools/pre-execute gate — the semantics of Gemini CLI's TOML policy files, expressed as plain plugin config (Schemastery-validated YAML, no code, no rule files).
Install
dsh plugin --profile my-agent add @amaster.ai/dsh-policy
Configuration
The plugin is disabled by default. Rules live in the profile's cordis.patch.yml like every other dsh plugin config (edits hot-reload with the config layer):
- name: '@amaster.ai/dsh-policy'
config:
enabled: true
rules:
- tool: '*' # every tool
decision: allow
priority: 20
- tool: bash # dsh's shell tool (pwsh works the same)
decision: deny
commandPrefix: npm
priority: 200
message: 'npm is not allowed. Use bun install / bun add / bun test instead.'
- tool: bash
decision: deny
commandPrefix: bun run
priority: 200
- tool: bash # …but one specific subcommand is fine
decision: allow
commandPrefix: bun run lint
priority: 300 # higher priority overrides the broader deny
- tool: bash # grep as a pipe filter stays allowed
decision: allow
commandPrefix: grep
priority: 300
- tool: write
decision: deny
argsPattern:
file_path: '\.md$' # matched against the file_path value directly
priority: 200
- tool: write # …except the project contract file
decision: allow
argsPattern:
file_path: 'PRODUCT\.md$'
priority: 300
- tool: write
decision: ask # resolved via ctx.approval (human/answerer chain)
argsPattern:
file_path: '(^|/)etc/'
message: 'writes to a system path'
Rule fields
| Field | Required | Meaning |
|---|---|---|
tool |
yes | Tool name or list of names; * matches every tool |
decision |
yes | allow runs the call, deny blocks it (the message reaches the model as the tool error), ask defers to dsh's approval seam |
priority |
no (0) | Higher priority wins among rules competing for the same command segment; ties break fail-closed (deny > ask > allow) |
message |
no | Deny reason / ask explanation (a generated default names the matched rule) |
argsPattern |
no | Regex (or list, any-of) matched against the JSON-stringified arguments — or a map of argument name → regex (or list): each key's pattern is matched against that argument's value (non-string values are JSON-stringified first), all keys must hold. Prefer the map form for field-targeted rules — no quote escaping, no key-order or cross-field accidents |
commandPrefix |
no | Anchored prefix match (or list, any-of) on each shell command segment, at a word boundary (npm matches npm install, never npmx) |
commandRegex |
no | Regex (or list, any-of) anchored at each shell command segment's start — Gemini-compatible; use .* to match mid-segment |
Plugin-level fields: enabled (master switch) and commandKeys (argument keys holding a shell command string — default ['command'], covering dsh's bash/pwsh tools).
Matching semantics
- Every condition on a rule must hold for the rule to match (AND). A rule with no conditions beyond
toolmatches every call of that tool. - Shell commands are checked segment by segment. Compound commands (
a && b | c, newlines, background&) are split, quote-aware, and$( )/ backtick substitutions are extracted as their own segments —cd /tmp && npm installandecho "$(npm install)"both hit thenpmrule. - Priority resolves competition within one segment; across segments the aggregation is conservative: any segment's
denydenies the whole call, then anyaskescalates, andallowrequires every segment decided allow. A broad deny is still overridable by a specific allow because both compete on the same segment (bun run lintat 300 vsbun runat 200). - No rule matches → the call passes through to the rest of the
tools/pre-executechain (next()), so dsh's own gates and other plugins keep their say. Invalid regexes are config errors and fail the plugin load; a runtime evaluation failure is logged with the[dsh-policy]prefix and delegates onward — the gate never breaks the agent loop.
What the gate covers
Everything registered in ctx.tools passes tools/pre-execute — the gate is tool-agnostic and needs no per-tool support:
- Official tools (verified against the 0.1.6-alpha.2 sources):
bash,pwsh,read,write,edit,read_image,web_search,web_fetch,list_subagent_modelsand the delegation tool,run_code, plus goal/skill/workflow/cordis tools. Match them bytool+argsPattern(the object form fits their argument shapes:file_pathforwrite/edit,urlforweb_fetch,queryforweb_search, …). commandPrefix/commandRegexapply to tools whose arguments carry a shell command string —bashandpwshboth usecommand(covered by the defaultcommandKeys).- PTC mode:
run_codesub-dispatches re-enter the scheduler'spreparestage, which runs the same pre-execute gate — rules apply per sub-call, not just perrun_code. - MCP tools bridged by
dsh-mcp-clientregister into the samectx.toolspipeline — match them by their registered names like any other tool.
The ask decision
ask is resolved by dsh itself: through the composed answerers of @deepseek-ai/dsh-user-approval (a UI prompt, an auto-answerer, …), failing closed to deny when no approval service is composed, and short-circuiting to reject under a session's approval/policy: never. The model-facing deny reason is the rule's message only when no approval service exists at all; a rejected/cancelled/unavailable outcome carries dsh-tools' own reason wording (verified against [email protected]). Gemini's modes (default/autoEdit/yolo/plan) have no dsh counterpart — dsh models that axis as the per-session approval policy (see dsh-permission-presets for the user-facing selector), and dsh profiles/cordis.patch.yml already scope config per deployment, so the plugin carries no mode axis of its own.
Gemini CLI policy mapping
| Gemini TOML | dsh-policy config |
|---|---|
toolName = "*" / "name" / ["a", "b"] |
tool: '*' / name / [a, b] |
decision = "allow" / "deny" / "ask_user" |
decision: allow / deny / ask |
priority = 300 |
priority: 300 (same direction) |
denyMessage |
message |
argsPattern (regex on the serialized args) |
argsPattern — plus a dsh-native object form { file_path: '…' } matching per-argument values (the string form keeps JSON.stringify key order, not Gemini's sorted-key form — patterns spanning multiple keys may need adjusting; the object form has no such issue) |
commandPrefix / commandRegex (single or list) |
same names, matched per command segment; commandRegex anchors at the segment start, like Gemini's at the command start |
modes = [...] |
no counterpart — use separate dsh profile patch rows |
allowRedirection |
not supported (dsh has no per-call redirection gate) |
Security
A policy plugin is advisory gating, not containment: deny rules keep a well-behaved agent off dangerous commands, but the agent process still runs with host privileges. Pair with dsh's sandbox stack (dsh-sandbox + a confining executor) for OS-level enforcement, and note that dsh ships no authentication or authorization of its own.
Compatibility
| @amaster.ai/dsh-policy | dsh | cordis |
|---|---|---|
| 0.1.x | `>=0.1.5-rc.1 |
Peer dependency: @deepseek-ai/dsh-tools (the tools/pre-execute gate). The approval seam (@deepseek-ai/dsh-user-approval) is optional and only involved in ask decisions.
License
MIT