# 🛡️ dsh-permission-rules
**Claude Code-style declarative permission rules for DeepSeek Harness.**
*Rules decide what is known. A reviewer model decides what is not.*
[](LICENSE)
[](https://github.com/topics/dsh-plugin)
[](#)
[](https://github.com/PerryLink/dsh-permission-rules/actions)
[](https://github.com/PerryLink/dsh-permission-rules/releases)
[](https://www.npmjs.com/package/dsh-permission-rules)
[](https://www.npmjs.com/package/dsh-permission-rules)
[English](README.md) · [简体中文](README.zh.md) · [Español](README.es.md) · [Português](README.pt.md) · [हिन्दी](README.hi.md)
---
## Compatibility
| Surface | Status |
|---|---|
| Harness | DeepSeek Harness `0.1.0-rc.5`–`0.1.0-rc.6` |
| Node | `^22.19.0 || >=24.0.0` |
| Platforms | All (host + web settings client) |
| Model | Any (deny/ask reasons surface through tool results) |
## What you get
`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:
- **`deny`** blocks the call; the rule's `reason` becomes the model-visible error.
- **`ask`** rides the official approval seam (mount `dsh-auto-review` for a second-model answerer, or a human answers; with neither, the harness fails closed).
- **`allow`** (and no-match) strictly delegates via `next()` — 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).
- **Rich matching** — tool-name globs (including `mcp__*`), agent-identity selectors (`main` / `subagent` / `preset:*`), argument key/value globs **or** regexes (with `!pattern` negation and an `absent` key dimension), workspace-relative path globs at **any nesting depth**, and `when` host conditions (env vars, platform).
- **Hierarchical rule files** — optional `searchUp` merges every `.dsh/rules.yaml` from the session cwd to the filesystem root, nearest first.
- **Dry-run rollout** — `enforce: false` audits what the policy *would* do while passing every call through.
- **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 more than `maxRules` rules fail the load.
## Rule syntax
```yaml
# /.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"
```
- **Match dimensions** — `tools` (globs, incl. `mcp__*`), `agents` (`main` / `subagent` / `preset:`; unknown identity never matches — fail closed), `params` (key/value globs or regexes, `!pattern` negation, `absent` key dimension), `paths` (workspace-relative globs extracted at any nesting depth), `when` (`env` var globs/regexes + a closed `platform` list), and `network` (`domains` / `ips` / `ports` / `schemes` — globs, wildcards, CIDRs, port ranges).
- **Actions** — `allow` / `deny` / `ask`, evaluated in file order, first match wins.
- **Rule metadata** — `enabled: false` (visible but inert), `description`, `tags`; unknown fields fail the load.
- **Schema** — a JSON Schema ships at [docs/rules-format.schema.json](docs/rules-format.schema.json) (editor completion via `# yaml-language-server: $schema=...`); the full vocabulary and a 5-rule security baseline live in [docs/rules-format.en.md](docs/rules-format.en.md).
## Network policy
A Codex-style **process-level network policy**: shell subprocess traffic flows through a built-in local **HTTP/CONNECT proxy**, and every connection is decided by ordered network rules or by three modes mapped onto the official sandbox presets:
- **`deny-all`** — the read-only sandbox preset: block all outbound.
- **`whitelist`** — the workspace-write preset: allow listed targets, `unlisted: ask` (or `deny`) for the rest.
- **`allow-all`** — the danger-full-access preset: allow everything.
- **`auto`** (default) — follows the sandbox preset; on hosts without the sandbox-policy service it resolves to `autoFallback` (`allow-all`).
- **Matching** — `match.network` with `domains` / `ips` / `ports` / `schemes` (globs, wildcards, CIDRs, port ranges; numeric YAML ports are accepted). URL-candidate extraction on the `tools/pre-execute` hot path fires on web-tool arguments and URLs embedded in bash/pwsh command text; loopback targets can short-circuit rules per `loopback` policy.
- **Audit** — denied connections append `permissionRules/network` to the owning session (same adaptive `ignorable` gate), with block counters and recent interceptions in `/rules network` and the settings page.
## Quick start
```sh
# 1. install the bundle into your profile
dsh plugin --profile web add "github:PerryLink/dsh-permission-rules#main"
# or from npm (published releases)
dsh plugin --profile web add dsh-permission-rules
# 2. restart and verify the row
dsh --profile web --dump-config | grep -A4 'id: permission-rules'
```
## Install & uninstall
- **git channel** (latest `main`): `dsh plugin --profile web add "github:PerryLink/dsh-permission-rules#main"` — the `prepare` script builds with production dependencies only.
- **npm channel** (published releases): `dsh plugin --profile web add dsh-permission-rules`.
- **tarball channel**: `pnpm pack` in this repo, then `dsh plugin --profile web add ./dsh-permission-rules-.tgz`.
- **uninstall**: `dsh plugin --profile web remove dsh-permission-rules`.
## 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; `ignore-with-warning` warns and continues empty |
| `maxRules` | `256` | Hard cap on rule count across the effective source chain |
| `maxCachedWorkspaces` | `512` | Hard cap on cached per-workspace rule loads (LRU eviction) |
| `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` |
| `caseInsensitivePaths` | *(win32)* | `paths` patterns and workspace-root comparison ignore ASCII case; `true` on Windows |
| `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 |
| `enforce` | `true` | `false` = dry-run mode: deny/ask hits are audit-logged with a `dryRun` marker and every call passes through |
| `allowUnmarkedAudit` | `false` | Pre-marker hosts drop the `ignorable` marker; the plugin disables session-log audit with a warning. Set `true` to opt back in |
| `network.enabled` | `true` | Master switch for the proxy, env injection, and web-tool mode defaults |
| `network.mode` | `auto` | Policy mode: `auto` follows the sandbox preset, or `deny-all` / `whitelist` / `allow-all` |
| `network.autoFallback` | `allow-all` | Mode used when `auto` has no sandbox-policy service |
| `network.unlisted` | `ask` | Whitelist-mode handling of targets no rule matched: `ask` or `deny` |
| `network.proxyBind` | `127.0.0.1` | Local proxy bind address (loopback only) |
| `network.proxyPort` | `0` | Local proxy port; `0` picks a free ephemeral port |
| `network.proxyMaxRecent` | `100` | Cap on recent-block records kept for the settings page |
| `network.loopback` | `allow` | Loopback targets: `allow` (Codex parity) or `policy` |
| `network.injectEnv` | `true` | Whether proxy environment variables are injected for subprocesses |
| `network.noProxy` | `clear` | Subprocess NO_PROXY handling: `clear` enforces the policy or `preserve` |
## Tools & surfaces
| Surface | Kind | Notes |
|---|---|---|
| `tools/pre-execute` | listener | First-match allow/deny/ask rules + network URL-candidate extraction |
| `/rules` | command | `list` · `reload` · `decisions [n]` · `test ` |
| `permissionRules/decision` | event | Log-only audit for every hit and passthrough |
| `permissionRules/network` | event | Proxy-layer audit for blocked connections |
| HTTP/CONNECT proxy | service | Built-in local proxy governing shell subprocess traffic |
| settings page | client | Network-mode editor, rule editor, block counters, recent interceptions |
```
/rules list the active rules, their source files, and any last-reload error
/rules list explicit alias for the bare listing
/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 dry-evaluate the rules against a hypothetical call
```
`/rules test` also accepts leading flags: `--cwd `, `--env KEY=VALUE` (repeatable), `--agent ` (repeatable), and `--platform `. In multi-file chains (e.g. `searchUp`), every listed rule line is attributed to its own source file.
## Permissions & data
- **Permissions**: declares `files:read`, `files:watch`, `files:write`, `session:append`, and `network:outbound` in its workshop manifest. `ask` decisions ride the official approval seam — nothing is re-implemented or bypassed.
- **Data**: rule files are read from disk; no rule data is written. No model calls, no reviewer subagents.
- **Session log**: `permissionRules/decision` is never injected into the model context and is appended with the envelope's `ignorable: true` marker so any harness build loads the log.
## Security boundaries
- **Policy, not a kernel.** `paths` candidates 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 `ask` decision 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.
- **Backtracking bounds.** Glob patterns are capped at `maxGlobStars` unbounded star expansions; regex-mode patterns reject nested unbounded quantifiers and quantified overlapping literal alternations.
## Known limitations
- **Audit marker on pre-marker hosts.** `permissionRules/decision` is appended with `ignorable: true`; hosts whose `Session.append` predates the marker (the `0.1.0-rc.6` line) silently drop it, so the runtime disables session-log audit with a one-time warning. Set `allowUnmarkedAudit: true` to opt back in; repair already-written logs with `scripts/repair-session-logs.mjs`.
- **Path candidates are heuristic.** Only the documented argument keys feed path matching, and workspace-relative matching is ASCII-case-insensitive only when `caseInsensitivePaths` is on.
- **Globs are a conservative subset.** No brace expansion — write two patterns, or use regex mode.
- **The regex backtracking guard is structural, not exhaustive.** Prefer glob mode for untrusted files.
## Collaborating with dsh-auto-review
- `dsh-permission-rules` produces `ask`; `dsh-auto-review` answers on the `approval/request` waterfall with a read-only second-model verdict (or delegates to humans). Mount both for the full closed loop.
- Integration-tested: `permissionRules/decision` → `approval/asked` → `autoReview/verdict` → `approval/decided`, with the reviewer replaced by a scripted mock.
- The `never` approval policy and every fail-closed guarantee of the official harness stay untouched.
## 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:
```sh
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`).
## Development
```sh
pnpm install # node ^22.19 || >=24
pnpm run typecheck # tsc, src + tests
pnpm run lint # eslint, src + tests + scripts
pnpm test # vitest: 139 tests, 9 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](VERIFICATION.md) for the headless end-to-end verification record.
## Topics
`dsh`, `dsh-plugin`, `deepseek-harness`, `permission`, `policy`, `allow-deny-ask`, `approval`, `safety`, `network`, `network-policy`, `proxy`
## Contributors
- [@PerryLink](https://github.com/PerryLink) — creator and maintainer: rule vocabulary and evaluation, runtime, HMR watch, session-log audit, network policy + proxy, and the five-language docs.
- [@22xuan](https://github.com/22xuan) — the detailed report on rc.6 hosts silently dropping the audit event's `ignorable` marker ([#2](https://github.com/PerryLink/dsh-permission-rules/issues/2)) and the upstream harness discussion; the v0.4.1 runtime host-capability detection and the documentation correction drew directly from that analysis.
## PerryLink DSH Plugin Family
This project is one of the [15 DeepSeek Harness plugins](https://github.com/PerryLink) maintained by [PerryLink](https://github.com/PerryLink). If this one helps you, the others likely will too:
| Plugin | One-liner |
|---|---|
| [dsh-mcp-panel](https://github.com/PerryLink/dsh-mcp-panel) | Read-only MCP runtime panel: /mcp command + Settings tab with status, tools and errors |
| [dsh-doublecheck](https://github.com/PerryLink/dsh-doublecheck) | Engineering-discipline guard: requirements grill, test gates, adversary review |
| [dsh-background-agents](https://github.com/PerryLink/dsh-background-agents) | Durable background child agents with a Web UI sidebar, messaging and interrupt |
| [dsh-lsp-actions](https://github.com/PerryLink/dsh-lsp-actions) | LSP diagnostics, formatting, completion, code actions and rename over language servers |
| [dsh-output-styles](https://github.com/PerryLink/dsh-output-styles) | Claude Code outputStyles-equivalent runtime style switching |
| [dsh-checkpoint-rewind](https://github.com/PerryLink/dsh-checkpoint-rewind) | Claude Code /rewind-equivalent: snapshots, session forks, one-shot restore |
| **[dsh-permission-rules](https://github.com/PerryLink/dsh-permission-rules)** | Claude Code-style declarative allow/deny/ask permission rules with audit |
| [dsh-auto-review](https://github.com/PerryLink/dsh-auto-review) | Second-model auto-review on the approval chain, fail-closed by default |
| [dsh-memento](https://github.com/PerryLink/dsh-memento) | Approval-gated cross-session memory: ctx.memory seam + SQLite + memory tool |
| [dsh-skill-pack-security](https://github.com/PerryLink/dsh-skill-pack-security) | Security-audit skill pack: secret scan, dependency and supply-chain review |
| [dsh-session-pin](https://github.com/PerryLink/dsh-session-pin) | Pin sessions in the Web sidebar with durable ordering |
| [dsh-composer-history](https://github.com/PerryLink/dsh-composer-history) | Terminal-style input history for the web composer: arrows, Ctrl+R search |
| [dsh-github](https://github.com/PerryLink/dsh-github) | GitHub PR/issues integration for DSH, every write gated by approval |
| [dsh-plugin-guide](https://github.com/PerryLink/dsh-plugin-guide) | Plugin-development knowledge base as an on-demand agent skill |
| [dsh-claude-move](https://github.com/PerryLink/dsh-claude-move) | Migrate Claude Code sessions, memory, skills and CLAUDE.md into DSH |
## License
[Apache License 2.0](LICENSE) © 2026 dsh-permission-rules contributors