dsh-project-prompt
Private, per-project prompt rules for DeepSeek Harness — matched by git remote/repo/path, worktree-aware, never committed to the repo
- Stars
- 2
- Language
- JavaScript
- Created
- Aug 26, 2026
- Updated
- Aug 26, 2026
Introduction
dsh-project-prompt
English | 中文
Private, per-project prompt rules for DeepSeek Harness (DSH).
Some agent instructions belong to your machine, not to the repository: environment-specific E2E procedures, internal-only endpoints, personal workflow preferences. AGENTS.md is committed and shared, so it is the wrong place for them. This plugin keeps such text in $DSH_HOME, never in the repo, and injects it into every session whose workspace matches a rule — including every subagent and every git worktree of the repository.
flowchart LR
subgraph repo["git repository (any clone / worktree / subdir)"]
wt1["main checkout"]
wt2["linked worktree A"]
wt3["worktree B"]
end
cfg["$DSH_HOME/cordis.patch.yml\nrules (machine-local, private)"]
plugin["dsh-project-prompt\nagent/session-start listener"]
sp["session system prompt\n(or first injected message)"]
cfg --> plugin
wt1 & wt2 & wt3 -->|match by remote / repo / path| plugin
plugin -->|section / inject| sp
Features
- Three matchers — match sessions by git
remoteURL (any clone, anywhere on disk), by local main-repo path, or by plain cwd prefix. Keys within a rule are OR-combined. - Worktree-aware — a linked worktree is traced back to its main repository through the
.gitfile'sgitdir:pointer, so a rule written once covers worktrees created later, and their subdirectories. - Two injection modes —
section(a stable system-prompt segment, KV-cache friendly, supports{{cwd}}/{{model}}) andinject(a first context message, no template interpolation — use it when the text contains literal{{...}}braces, e.g. Helm or Go templates). - Covers subagents — subagents inherit the session cwd, so delegated work follows the same rules.
- Fails loudly at load time — malformed rules (missing
text, unknown{{var}}in section mode) throw when the plugin loads, not on every request. - Zero dependencies — a single ESM file importing only Node builtins; no build step, nothing to compile.
Requirements
- DeepSeek Harness ≥
0.1.1-rc.2(uses theagent/session-startevent,systemPrompt.section, andagent.injectextension points). - Node.js ≥ 18 (DSH itself currently requires ≥ 22).
Install
Install into a profile with DSH's plugin command:
dsh plugin --profile web add dsh-project-prompt
# or install straight from the GitHub repository:
dsh plugin --profile web add github:imroc/dsh-project-prompt
Restart DSH afterwards — bundles are composed at startup.
Uninstall:
dsh plugin --profile web remove dsh-project-prompt
Configure
Rules live in $DSH_HOME/cordis.patch.yml (~/.dsh/cordis.patch.yml by default) — the machine-local layer that applies to every profile on the machine. Override the row the bundle installed:
- id: project-prompt
config:
rules:
# Matches this repository wherever it is cloned, and in all worktrees.
- remote: github.com/my-org/my-project
text: |
## E2E testing for this project (environment-specific, private)
1. Test entry point: http://e2e.internal.example.net (internal only)
2. Run `make e2e-prepare` before the first E2E run.
3. On failures, check ... first.
# Matches by local main-repo path; the rule text contains literal
# {{...}} braces, so it must use inject mode.
- repo: /home/me/dev/another-project
mode: inject
text: |
Render values like {{ .Values.replicas }} are literal here.
# Matches any session started under a plain directory prefix.
- path: /home/me/dev
text: |
...
The file is watched for changes — saving it hot-reloads the plugin row (take effect for sessions started afterwards).
Rule reference
| Key | Type | Default | Description |
|---|---|---|---|
remote | string | — | Git remote URL (origin) to match. Compared after normalization: scheme, user@, scp-style : separator, trailing .git and slashes, and case are ignored. git@github.com:u/r.git ≡ https://github.com/u/r. A bare host/org/repo suffix also matches. |
repo | string | — | Local absolute path of the git main repository. Any linked worktree and any subdirectory of it matches (worktrees are traced back via the .git gitdir pointer). |
path | string | — | Cwd prefix: the session workspace equals this directory or lives underneath it. |
text | string | required | The prompt text to inject. |
mode | section | inject | section | Injection mode, see below. |
sectionName | string | project-prompt (auto-increments when several section rules match one session) | System-prompt section name. |
order | number | 50 | System-prompt section order (DSH conventions: 0 persona, 100–199 tool guidance). |
A rule needs at least one of remote / repo / path; several matching rules all apply.
Injection modes
section (default) | inject | |
|---|---|---|
| Lands as | System-prompt segment on the agent-scoped systemPrompt service | First user-side context message (same path AGENTS.md content takes) |
| Visible | Every request of the session | Every request of the session |
| Template interpolation | Yes — {{cwd}} and {{model}} resolve; any other complete {{...}} group throws | No — braces are literal |
| Best for | Stable instructions; KV-cache-friendly | Text containing literal {{...}} (Helm/Go templates, Terraform, etc.) |
The plugin validates section text at load time: an unknown {{var}} fails the load with a message telling you to switch that rule to mode: inject.
How it works
- DSH emits
agent/session-startsynchronously before the first model request of every new session (fresh, resumed, after clear/compact — each publish mints a new agent scope). - The plugin reads
agent.session.header.cwdand tests each rule; git repository identity is resolved by walking up from the cwd to.git(a file means a linked worktree, whosegitdir:pointer leads back to the main repository root and itsremoteURLs). - On match, the rule's text is registered on
agent.ctx— the agent-scoped context — so it applies to that agent only and is discarded with it. - Every model request of that session re-assembles the system prompt (or re-reads the injected message), so the rule text is present throughout.
"Model-visible means logged": injected sections appear in the request headers recorded in the session log ($DSH_HOME/sessions/…), which is also how you verify a rule landed.
Verify an installation
- Check the row composed:
dsh --profile web --dump-config | grep -A3 project-prompt. - Start a session in a matched directory and ask the model a question the rule text should influence.
- Or inspect the recorded prompt: the session's
session.jsonl.zstdcontains the assembled system prompt with your section.
Limitations
- Matching is identity-by-path or by-remote, not by content: two different clones of the same remote both match a
remoterule (that is usually the point). - Submodule directories (
.gitpointing into.git/modules/…) are not traced; use apathrule for them. - Rules are static configuration — this plugin intentionally does not read files from inside the repository (that would reintroduce shared state). For workspace-local files, see DSH's built-in
AGENTS.md/CLAUDE.mdloading.
Development
git clone github:imroc/dsh-project-prompt && cd dsh-project-prompt
npm test # node --test; git fixtures are created in a temp dir
node --check index.js
The plugin is a single dependency-free ESM file; the committed source is the shipped artifact (no build, no prepare script — which is also why git installs need no pnpm allowBuilds entry). See AGENTS.md for the constraints that keep it that way.