PerryLink
dsh-permission-rules
Claude Code-style declarative permission rules for DeepSeek Harness: ordered allow/deny/ask rules with tool-name, argument (glob/regex), and workspace-path matching on the tools/pre-execute waterfall, session-log audit, and HMR reload.
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 13, 2026
- Updated
- Aug 14, 2026
Introduction
๐ก๏ธ dsh-permission-rules
Claude Code-style declarative permission rules for DeepSeek Harness.
Rules decide what is known. A reviewer model decides what is not.
English ยท ็ฎไฝไธญๆ ยท Espaรฑol ยท Portuguรชs ยท เคนเคฟเคจเฅเคฆเฅ
What it does
dsh-permission-rules puts an ordered allow / deny / ask rule list in front of every tool call on the tools/pre-execute waterfall โ deterministic, instant, auditable, and written by you in plain YAML:
denyblocks the call. The rule'sreasonbecomes the model-visible error, so the agent learns why instead of retrying blindly.askrides the official approval seam. Mountdsh-auto-reviewalongside and the question is settled by a second model; otherwise a human answers; with neither, the harness fails closed.allow(and no-match) strictly delegates vianext()โ downstream listeners are never short-circuited.
Every hit and every passthrough is audit-logged as a permissionRules/decision session event (log-only โ nothing extra is injected into the model context).
tools/pre-execute waterfall approval/request waterfall (answerer chain)
โ โ
dsh-permission-rules dsh-auto-review answerer
ยท first-match rules in file order โโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโ
ยท deny/ask claim the call โ AI verdict (second model) โ no โโ next() โโโถ human UI
ยท allow/passthrough โ next() โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ deny โโโถ denied tool result โ allowed-once / rejected
โ ask โโโถ ctx.approval โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
audit: permissionRules/decision โ approval/asked โ autoReview/verdict โ approval/decided
Why rules and a reviewer?
A second model answers "is THIS call okay?" with judgment, but costs a round-trip and can be wrong. Declarative rules answer deterministically, instantly, and without a model โ but only cover what an admin wrote down. Combined, you get the "rules first, AI backstop" loop: rules decide the known, the reviewer decides the unknown.
Features
- โ
Three-state semantics โ
allow,deny,ask, evaluated in file order, first match wins - โ
Rich matching โ tool-name globs (including
mcp__*), argument key/value globs or regexes (with!patternnegation and anabsentkey dimension), workspace-relative path globs extracted from documented argument keys at any nesting depth, andwhenhost conditions (env vars, platform) - โ
Hierarchical rule files โ optional
searchUpmerges every.dsh/rules.yamlfrom the session cwd to the filesystem root, nearest first, so a child project can override parent rules - โ
Rule metadata โ
enabled: false,description,tags;/ruleswarns about rules shadowed by an earlier catch-all - โ
Waterfall-safe โ
allow/passthrough always callnext(); onlydeny/askshort-circuit - โ
Official approval seam โ
askflows throughctx.approval; never re-implemented, never bypassed - โ
Full audit โ
permissionRules/decisionevents carry the rule action AND the final outcome for every call;/rules decisionsreplays the trail in-session - โ
Dry-run testing โ
/rules test <tool> <json-args>evaluates the active rules without executing anything - โ Hot reload โ Chokidar watch with debounce; a broken edit keeps the previous rules, never crashes
- โ
Fail loud โ invalid YAML, unknown actions/fields, bad globs/regexes, backtracking-prone patterns, or >
maxRulesfail the load - โ
Bounded hot path โ precompiled matchers, O(rules ร patterns), capped by
maxRules; glob backtracking degree capped bymaxGlobStars
Quick start
# 1. install the bundle into your profile
dsh plugin --profile web add "github:PerryLink/dsh-permission-rules#main"
# or from a packed tarball (built artifacts, no build permission needed)
pnpm pack
dsh plugin --profile web add ./dsh-permission-rules-0.1.1.tgz
# 2. restart
dsh --profile web
Then create the rules file for your project and start a session in it:
# <project>/.dsh/rules.yaml
rules:
- match: { tools: [bash, pwsh], params: { command: "git push*" }, paths: ["**/secrets/**"] }
action: deny
reason: "No pushes from protected paths"
- match: { tools: [edit, write] }
action: ask
reason: "File writes need confirmation"
dsh --profile web --dump-config | grep -A4 'id: permission-rules' # verify the row
A complete 5-rule security baseline and the full schema live in docs/rules-format.en.md.
Configuration
All tunables are Schemastery Config fields (changeable from cordis.yml). An id-targeted override replaces the whole row โ restate every key you need.
| Key | Default | Meaning |
|---|---|---|
rulesFile | .dsh/rules.yaml | Rule file location; relative = resolved against the calling session's cwd, absolute = global and validated at mount |
fallbackPath | (none) | Rule file used when per-cwd discovery finds nothing; validated at mount |
badFilePolicy | fail | Bad rule file: fail errors the pending tool call loudly (reloads keep the previous rules); ignore-with-warning warns and continues empty |
maxRules | 256 | Hard cap on rule count across the effective source chain; larger files fail the load |
maxCachedWorkspaces | 512 | Hard cap on cached per-workspace rule loads; the least-recently-used workspace (and its watcher) is evicted beyond it |
patternMode | glob | params/paths/when.env pattern flavor: glob or regex (tool names are always globs) |
watch | true | Chokidar watch + reload on change |
watchStabilityThresholdMs | 200 | Reload debounce window (ms) |
language | en | /rules output language: en, zh, es, pt, hi (en/zh are the reference translations) |
caseInsensitivePaths | (win32) | paths patterns and workspace-root comparison ignore ASCII case; defaults to true on Windows, false elsewhere |
audit | all | Audit granularity: all logs every hit AND passthrough; hits skips passthrough events |
searchUp | false | Walk parent directories from the session cwd and merge every found rule file, nearest first |
maxGlobStars | 2 | Hard cap on unbounded */** quantifiers per glob pattern (backtracking-degree bound) |
Session commands
/rules list the active rules, their source files, and any last-reload error
/rules reload re-read the rule-file chain for this workspace
/rules decisions [n] show the last n permission decisions of this session (default 10)
/rules test <tool> <json> dry-evaluate the rules against a hypothetical call, e.g. /rules test bash {"command":"git push origin main"}
Command output is UI-only โ the model learns the rules only through the tool results they produce. language picks the output language. A JSON Schema for the rule file ships at docs/rules-format.schema.json (wire it up with # yaml-language-server: $schema=... for editor completion).
Collaborating with dsh-auto-review
dsh-permission-rulesproducesask;dsh-auto-reviewanswers on theapproval/requestwaterfall with a read-only second-model verdict (or delegates to humans). Mount both for the full closed loop.- Integration-tested (
test/integration.spec.ts):permissionRules/decisionโapproval/askedโautoReview/verdictโapproval/decided, with the reviewer replaced by a scripted mock. - The
neverapproval policy and every fail-closed guarantee of the official harness stay untouched.
Security boundaries
- Policy, not a kernel.
pathscandidates come only from a documented set of argument keys (at any nesting depth, depth-capped), and only workspace-relative paths match. - No reviewer here. The plugin never spawns subagents or calls models โ producing an
askdecision is the end of its work. - No sandbox changes. OS-level sandbox policy belongs to the sandbox seam, not this plugin.
- Loud misconfiguration. Unknown YAML fields, unknown actions, and bad patterns are rejected at load, never silently ignored.
- Backtracking bounds. Glob patterns are capped at
maxGlobStarsunbounded star expansions; regex-mode patterns reject nested unbounded quantifiers and quantified overlapping literal alternations. (Regex chains like\d+\.\d+\.\d+stay allowed โ regex mode is the escape hatch, glob mode is the guarded default.)
Related work
- Andy8647/dsh-auto-approval โ two-state allow/deny classifier with its own file-log audit; this plugin adds the full three-state semantics, declarative YAML rules, session-log audit, and
next()-safe delegation. Drifter-yh/dsh-tool-policyโ deny-by-default tool policy; documented here to avoid duplicate implementation.dsh-auto-reviewโ the AI-backstop half of the loop this plugin fronts.
Known limitations
permissionRules/decisionis appended with the envelope'signorable: truemarker, so any harness build loads the log โ readers that do not know the out-of-repo type simply skip the audit record instead of refusing the session. (rc.6 hosts accept and ignore the marker, keeping the exact pre-marker behavior; sessions written on rc.6 hosts lack the marker and may needscripts/repair-session-logs.mjsbefore loading on hosts with required-on-read semantics.)pathscandidates are heuristic: only the documented argument keys feed path matching, and workspace-relative matching is ASCII-case-insensitive only whencaseInsensitivePathsis on.- Globs are a conservative subset (no brace expansion) โ write two patterns, or use regex mode.
- The regex backtracking guard is structural, not exhaustive: alternation-ambiguity cases without literal prefixes (e.g. crafted lookarounds) are the author's responsibility; prefer glob mode for untrusted files.
Session log repair
Session logs written before the ignorable marker existed can be refused by newer harness builds (SessionFormatUnsupportedError). The shipped scripts/repair-session-logs.mjs rewrites only the targeted audit rows to carry ignorable: true, frame-preserving, with backups:
node scripts/repair-session-logs.mjs scan [--home DIR] # report foreign rows, change nothing
node scripts/repair-session-logs.mjs repair [--home DIR] [--dry-run]
--home defaults to $DSH_HOME/sessions (or ~/.dsh/sessions). See the script header for the full contract.
Development
pnpm install # node ^22.19 || >=24
pnpm run typecheck # tsc, src + tests
pnpm run lint # eslint, src + tests + scripts
pnpm test # vitest: 106 tests, 8 suites
pnpm run test:coverage # coverage gate (90/80/90/90)
pnpm run build # tsc declarations + tsdown bundles (lib/)
pnpm run pack:check # build + pack (the published artifact)
node scripts/check-readme-sync.mjs # five-language README sync gate (also in CI)
See VERIFICATION.md for the headless end-to-end verification record (deny blocking a shell tool, ask routing through the approval seam, --dump-config).
License
Apache License 2.0 ยฉ 2026 dsh-permission-rules contributors