Back to home@imroc

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
GitHub repo

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 remote URL (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 .git file's gitdir: pointer, so a rule written once covers worktrees created later, and their subdirectories.
  • Two injection modessection (a stable system-prompt segment, KV-cache friendly, supports {{cwd}}/{{model}}) and inject (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 the agent/session-start event, systemPrompt.section, and agent.inject extension 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

KeyTypeDefaultDescription
remotestringGit 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.githttps://github.com/u/r. A bare host/org/repo suffix also matches.
repostringLocal 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).
pathstringCwd prefix: the session workspace equals this directory or lives underneath it.
textstringrequiredThe prompt text to inject.
modesection | injectsectionInjection mode, see below.
sectionNamestringproject-prompt (auto-increments when several section rules match one session)System-prompt section name.
ordernumber50System-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 asSystem-prompt segment on the agent-scoped systemPrompt serviceFirst user-side context message (same path AGENTS.md content takes)
VisibleEvery request of the sessionEvery request of the session
Template interpolationYes — {{cwd}} and {{model}} resolve; any other complete {{...}} group throwsNo — braces are literal
Best forStable instructions; KV-cache-friendlyText 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

  1. DSH emits agent/session-start synchronously before the first model request of every new session (fresh, resumed, after clear/compact — each publish mints a new agent scope).
  2. The plugin reads agent.session.header.cwd and tests each rule; git repository identity is resolved by walking up from the cwd to .git (a file means a linked worktree, whose gitdir: pointer leads back to the main repository root and its remote URLs).
  3. 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.
  4. 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

  1. Check the row composed: dsh --profile web --dump-config | grep -A3 project-prompt.
  2. Start a session in a matched directory and ask the model a question the rule text should influence.
  3. Or inspect the recorded prompt: the session's session.jsonl.zstd contains 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 remote rule (that is usually the point).
  • Submodule directories (.git pointing into .git/modules/…) are not traced; use a path rule 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.md loading.

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.

License

MIT © roc