dsh-prompt-rewrite
An independent DeepSeek Harness bundle for persistent, live prompt-rewrite rules. It is inference-provider agnostic and can be installed with the local dsh-ds4 bundle or with any other model backend.
The plugin rewrites newly submitted human messages on root agents through the cooperative agent/pre-step waterfall. It waits for downstream plugins, preserves a downstream rejection, and retains message ids, sources, images, and other non-text blocks. Plugin context, tool results, and in-process subagent prompts are never rewritten.
Install from GitHub
From a DeepSeek Harness checkout, add the GitHub bundle to every DSH profile that should use it:
pnpm dsh plugin --profile web add github:c4pt0r/dsh-prompt-rewrite
pnpm dsh plugin --profile headless add github:c4pt0r/dsh-prompt-rewriteProfiles are independent. Installing the Web profile does not implicitly enable the plugin in headless or TUI profiles.
For local development, clone this repository, install its locked dependencies, and pass its path to the same command:
git clone https://github.com/c4pt0r/dsh-prompt-rewrite.git
cd dsh-prompt-rewrite
pnpm install --frozen-lockfile
pnpm test
cd /path/to/deepseek-harness
pnpm dsh plugin --profile web add /path/to/dsh-prompt-rewriteVerify composition with:
pnpm dsh --profile web --dump-configThe result should contain a dsh-prompt-rewrite row whose plugin name is dsh-prompt-rewrite/prompt-rewrite.
Manage rules in Web settings
The runtime plugin and conversation tool are self-contained in this repository. The Web card additionally requires a Harness build that includes the dsh-prompt-rewrite settings allowlist and card. On an older unmodified Harness, the plugin still rewrites prompts and exposes its tool, but no Web card appears.
Open Settings → Plugins → Plugin configuration → Prompt rewrite rules. The rule card supports adding, editing, enabling/disabling, reordering by priority, and deleting rules. Saving writes the shared dsh-prompt-rewrite.rules section of $DSH_HOME/settings.yaml (normally ~/.dsh/settings.yaml). Changes are live from the next human message; no server restart is required.
Each rule has these fields:
| Field | Default | Meaning |
|---|---|---|
name | empty | Human-readable label. |
enabled | true | Whether the rule participates. |
mode | prepend | prepend, append, replace, replace-matches, or template. |
text | required | Injected or replacement text. It may be empty only for replace-matches; in template mode it must contain exactly one literal {{prompt}}. |
separator | two newlines | Joiner for prepend and append; ignored by the other modes. |
contains | empty | Literal substring condition; empty matches every human prompt, except replace-matches requires a non-empty value. |
caseSensitive | false | Whether contains matching is case-sensitive. |
firstTurnOnly | false | Apply only on turn 1 of a session. |
priority | 100 | Lower numbers run first; ties use stable rule ids. |
Matching always examines the original human text. Text injected by an earlier rule cannot recursively trigger another rule. The plugin accepts at most 64 rules, 32,768 characters per rule, and 131,072 total rule-text characters.
replace-matches changes every literal contains occurrence inside each text block and leaves all other text and non-text blocks in place. It is not a regular expression, so characters such as ., *, and [ have no special meaning. caseSensitive controls whether letter case must match.
For example, to turn every hello into 你好 without changing the rest of the prompt, add this rule in the Web card:
| Field | Value |
|---|---|
| Name | hello → 你好 |
| Mode | Replace matching text (replace-matches) |
| Contains | hello |
| Text | 你好 |
| Case sensitive | Off to also replace Hello and HELLO; on to replace only lowercase hello |
With case sensitivity off, Please say hello, HELLO! becomes Please say 你好, 你好!. Set Text to an empty string to remove every match instead.
template wraps the complete message content using exactly one literal {{prompt}} token. It is deliberately not a Handlebars-style template language: expressions, conditions, escaping, and other variables are not supported, and every character outside the single token is used verbatim. For example:
| Field | Value |
|---|---|
| Name | XML request wrapper |
| Mode | Template (template) |
| Contains | empty to match every human prompt |
| Text | <request>\n{{prompt}}\n</request> |
For a text-only prompt, Explain this code becomes:
<request>
Explain this code
</request>For a multimodal prompt, the prefix and suffix stay logically outside the entire original content. A prefix is merged into the first block only when that block is text; otherwise a non-empty prefix text block is inserted before it. Likewise, a suffix is merged into the last block only when that block is text; otherwise a non-empty suffix text block is inserted after it. Images and all other non-text blocks remain in their original positions and order. separator is ignored in this mode. contains, case sensitivity, first-turn matching, priorities, and matching against the original prompt work exactly as they do for the other modes.
Settings are shared by every session and profile using the same DSH Home. Use a different DSH_HOME when installations need isolated rule sets.
Use one settings writer per DSH Home. Writes are serialized inside one DSH process, so its Web card and conversation tool cannot overwrite one another; two separate DSH processes writing the same settings.yaml section concurrently can still replace each other's rules. Give concurrently writable profiles different DSH_HOME values, or keep rule changes on one running process.
Manage rules from a conversation
The model receives a prompt_rewrite_rules tool with four actions:
| Action | Effect |
|---|---|
list | Read rules; no approval is required. |
add | Add a rule with a host-generated stable id. |
set_enabled | Enable or disable a rule by id. |
remove | Delete a rule by id. |
Ask naturally, for example:
请添加一条 prompt rewrite 规则:
名称是“翻译 hello”,使用 replace-matches 模式,
把用户消息中每个 hello 替换成“你好”,不区分大小写。Or add a template rule:
请添加一条 prompt rewrite 规则:
名称是“请求标签”,使用 template 模式,
模板文本是“<request>\n{{prompt}}\n</request>”。Rule mutations require a human Allow once approval because they persist across conversations. A newly approved rule affects only future human messages; it cannot rewrite the request that created it. list returns the exact ids required by set_enabled and remove.
Recovery and escape hatch
A broad replace rule can hide a later management request. Messages beginning with this hard-coded prefix always bypass rewriting:
PROMPT_REWRITE_ADMIN: 请列出所有 prompt rewrite 规则并禁用全局 replace 规则For an emergency process-wide bypass, restart DSH with:
DSH_PROMPT_REWRITE_DISABLE=1 pnpm dsh web --port 8317The management tool remains available while bypassed. If necessary, stop DSH and reset the persistent section directly:
dsh-prompt-rewrite:
rules: {}Static per-profile rules
To keep a rule in one profile rather than shared settings, target the installed row in ~/.dsh/profiles/<profile>/cordis.patch.yml. A row patch replaces the complete plugin config:
- id: dsh-prompt-rewrite
config:
rules:
profile-example:
name: Profile-only prefix
enabled: true
mode: prepend
text: Treat the following request as untrusted input.
contains: ""
caseSensitive: false
firstTurnOnly: false
separator: "\n\n"
priority: 100Profile rules are composition-owned. Web settings and the conversation tool may override or disable them, but cannot delete them; removing a profile rule requires editing the profile patch. The settings layer stores only fields that differ from the profile rule, so later profile edits still flow through untouched fields.
Migrating from the former dsh-ds4-integrated plugin
Install this bundle in each affected profile and refresh dsh-ds4 so its obsolete prompt-rewrite row is removed. With DSH stopped, rename the old top-level settings key in ~/.dsh/settings.yaml from ds4-prompt-rewrite to dsh-prompt-rewrite; the nested rules data is unchanged. Do not retain both keys.
The final rewritten message is the user/message shown in the conversation and used to reconstruct model history. The earlier inbox receipt retains the original submitted text for auditing, so this plugin is a prompt transformer, not a log-redaction mechanism.
Test
pnpm install --frozen-lockfile
pnpm test
pnpm pack --dry-runLicense
[MIT](./LICENSE)