DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Custom Header — DeepSeek Harness 插件(DSH Plugin)
← Plugins
C

dsh-custom-header

Custom Header

在 fetch 传输层修改 DeepSeek Harness (DSH) 的出站 LLM 请求:注入客户端身份标头预设(Codex / Claude Code / opencode)、移除 X-Stainless 指纹、重写 URL,并在请求中内联修补 Anthropic 请求体;与提供商无关,并按主机允许列表限制

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:Asaiuta/dsh-custom-header#40d3d21e9736db5936ab6638f9bca966d0ea737c
README兼容性版本

说明

在 fetch 传输层修改 DeepSeek Harness (DSH) 的出站 LLM 请求:注入客户端身份标头预设(Codex / Claude Code / opencode)、移除 X-Stainless 指纹、重写 URL,并在请求中内联修补 Anthropic 请求体;与提供商无关,并按主机允许列表限制。

兼容性与来源证明

Custom Header 以 dsh-custom-header 发布,当前版本为 0.4.1。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/10/17

版本

0.4.1stable
2026/10/17
0.4.0stable
2026/10/16

相关插件

正在加载相关插件…

最新版
0.4.1
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 3
周下载
0
最近提交
2026/10/17
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录

相关插件

继续浏览 security-access 分类下经过校验的插件。

Cc Safety Netcc-safety-net编码代理 CLI 钩子——阻止破坏性命令和访问机密文件Pocketdsh-pocket把 DeepSeek Harness 装进你的口袋:一个包、一个设置页,手机扫码即同步访问电脑上的 DSH(局域网 + 公网,实时同屏)。550c Bootdsh-550c-boot550C 开机动画 + 登录授权界面:启动时播放 550C 片头,之后可走一段可交互的访问授权(UEG 联合政府 / 550C 控制终端两套主题),点击或 Enter 推进、Esc 跳过;通用设置里可切换或关闭。Auto Reviewdsh-auto-review针对 DeepSeek Harness 审批请求的第二模型 AI 自动审查:只读审查子代理在审批应答链上决定允许或拒绝,并采用故障关闭回退机制和完整的会话日志审计。

README

dsh-custom-header

Outbound LLM request modification for DeepSeek Harness (DSH), at the fetch transport layer.

One globalThis.fetch middleware chain (keyed by a DSH-scoped symbol, Symbol.for("dsh-custom-header.fetch.pipeline.v1")) that every provider — whatever plugin owns it — streams through:

  • injects client-identity headers (User-Agent, Originator, X-Claude-Code-Session-Id, x-opencode-*, …) from presets that mirror real Codex / Claude Code / opencode clients
  • strips X-Stainless-* runtime fingerprint headers
  • rewrites the request URL (path matching + appendQuery)
  • patches request bodies: the Anthropic Messages identity / billing system block + metadata.user_id, and the opencode Zen free-tier tool stubs

For servers you administer or are authorized to use. Cloudflare TLS/Bot edge blocks need a different API entry, an admin allowlist, or a local forward proxy.

Why fetch, not a provider-owning adapter

The 0.2/0.3 line rewrote this plugin as provider-owning LlmAdapters registered through ctx.llm.registerAdapter(routes, adapter). That extension point is per-route exclusive: registering a route another plugin already owns (say the ziyong/moonshotai providers that llm-pi-ai registers) throws DUPLICATE_ADAPTER, and the host exposes no way to graft onto someone else's adapter. The point of this plugin is exactly "make requests to my gateway look like client X" — whatever plugin those requests come from — so it needs a seat that owns no routes at all. The fetch layer is that seat: it intercepts the transport every adapter already uses.

The cost of the fetch seat is that only the request URL identifies a request (never the owning provider). So the gatewayHosts allowlist is the single safety boundary: every profile — fixed ones included — touches only requests whose host matches, and an empty allowlist is a safe no-op.

How it works

apply registers four middlewares into the shared pipeline, each re-reading a snapshot that loader/volatile-update refreshes (a settings save therefore lands in the next request — no restart):

prioritymiddlewarewhat it does
5url-rewritepath matching + appendQuery / replacement, host-gated
6header-stripX-Stainless-* and runtime fingerprint removal, on profiles that ask for it
7header-injectthe resolved profile's identity headers + extraHeaders
8body-patchAnthropic identity body patch; opencode Zen /zen → /inference + bash/read tool stubs (free models only)

The chain mechanics: an accessor fetch on globalThis whose setter keeps the underlying fetch in sync; middlewares compose on every (re)install, priority ascending; each receives { input, init, next } and may call next with rewritten input. The pipeline is vendored from @aizigao/pi-fetch-pipeline (MIT), re-keyed for DSH so multiple plugins can coexist without clobbering each other's chains.

Session-scoped ids (x-opencode-session, X-Claude-Code-Session-Id, metadata.user_id) cannot be read from a URL, so the plugin wraps the llm/stream waterfall: every adapter stream iteration — including the fetch it issues — runs inside an AsyncLocalStorage carrying GenerateOptions.sessionId. That is what keeps concurrent DSH conversations from sharing one process-wide id. The same observer logs a 403 hint distinguishing Cloudflare HTML edge blocks from JSON client-restriction rejections.

Configuration is a schemastery Config schema on the plugin, validated by the Loader against cordis.yml. Every field is .volatile(), which is what makes dsh-settings publish a writable namespace for the entry at all: its describe() collects only entries carrying a volatile field, and its writer rejects any other path. A volatile-only change is committed into the running fiber rather than remounting it — the host half re-reads its refs on loader/volatile-update instead of snapshotting at mount.

The page itself is the plugin's own browser half (src/client/*). It registers as a section of the Settings panel (settings.section) and, as a second seat, on the bundle's own row in the Plugins page (plugins.row.config) — see Configuration page below.

concernwhere
middleware registration + diagnosticsindex.ts
fetch pipeline (vendored, symbol-keyed)fetch-pipeline.ts
header injection / strippingheader-inject.ts
profile resolution + presetsprofiles.ts
Anthropic body patch + Zen free tier wiringbody-patch.ts
opencode Zen helpersopencode-zen.ts
URL rewriteurl-rewrite.ts
per-conversation session idssession-context.ts (AsyncLocalStorage)
403 diagnosticsindex.ts + response-hints.ts
settings schema (all volatile)config.ts (ConfigSchema, schemastery)
configuration pageclient/index.tsx (settings.section + plugins.row.config)
scenario tabs → staged profileclient/scenarios.ts
page copy (zh / en)client/locales.ts
page controls + field specsclient/fields.tsx
page stylesheetclient/styles.ts

Safe default: profile: "off", or an empty gatewayHosts — either way nothing is injected, stripped, rewritten or patched.

Profiles

Each fixed profile is a complete client-identity header set:

ProfileHeaders sent
codex_desktopcodex_app/1.2026.0628 (Windows NT 10.0; Win64; x64) + Originator: codex_app
codex_officialcodex_cli_rs/0.147.0 (Windows 10.0.19045; x86_64) WindowsTerminal + Originator: codex_cli_rs
codex_tuicodex-tui/0.147.0 (…; x86_64) WindowsTerminal + Originator: codex-tui
codex_claude_pluginClaude Code/0.5.0 (Macos 15.5; arm64) iTerm2.app + Originator: Claude Code
pi_agentpi-coding-agent/1.0 + Originator: pi (needs the server to accept this identity too)
claude_code_messagesclaude-cli/2.1.220 (external, sdk-cli) + X-Claude-Code-Session-Id + full anthropic-beta + body (metadata.user_id JSON / system block)
opencode_zenopencode/1.19.0 ai-sdk/... UA + x-opencode-client/project/session/session-id/request (descending ses_ id)
autoon a matching host: Anthropic /v1/messages paths → Claude set; anything else → codexFallback
offnothing

All profiles — fixed ones included — are gated by gatewayHosts. The Codex header values are verified against openai/codex source (see "Truthfulness verification"); the opencode Identifier id scheme (msg_ ascending / ses_ descending, hex12 + base62) and the Claude Code 2.1.220 header set are re-implemented from their real-world formats.

Truthfulness verification (Codex presets)

Codex identity fields were checked against primary sources:

  • Originator: codex_cli_rs — openai/codex codex-rs/login/src/auth/default_client.rs: DEFAULT_ORIGINATOR = "codex_cli_rs".
  • codex_official UA shape — same file, get_codex_user_agent(): {originator}/{CARGO_PKG_VERSION} ({os_type} {os_version}; {arch}) {terminal}, with the terminal token from codex-rs/terminal-detection/src/lib.rs (WindowsTerminal for Windows Terminal, xterm-256color from TERM, …). A literal terminal tail matches no real build and was replaced. 0.147.0 is the current release (rust-v0.147.0, 2026-08-07).
  • codex_cli_rs vs codex-tui — the CLI has several first-party front-ends. default_client.rs sets DEFAULT_ORIGINATOR = "codex_cli_rs" and builds the UA from the process originator, but is_first_party_originator also whitelists codex-tui and codex_vscode, and the TUI self-identifies as client_name: "codex-tui" (tui/src/lib.rs). Interactive codex sessions therefore actually send codex-tui/… UA + Originator: codex-tui. Profiles codex_official (headless/default) and codex_tui (interactive TUI) cover both.
  • codex_app/ + Originator: codex_app — the desktop shell is closed-source; codex_app is the client family's own identity string. The full UA is a measured capture; override via codexDesktopVersion.
  • codex_claude_plugin UA — Claude Code UA format (Claude Code/x.y.z (OS; arch) Terminal); 0.5.0 is an old but real version. Server checks only match the prefix, so the value is kept conservative.

Install

dsh plugin --profile web add file:$(pwd)     # or: dsh plugin add dsh-custom-header

Verify: look for the dsh-custom-header loaded: ... diagnostics in the server log, or inspect ctx.dshCustomHeader.status().

Config (cordis.yml)

dsh-custom-header:
  profile: auto             # auto | off | codex_desktop | codex_official | codex_tui | codex_claude_plugin | pi_agent | claude_code_messages | opencode_zen
  gatewayHosts:
    - gateway.example.com   # the allowlist: EVERY profile only touches these hosts; subdomains match; empty = no-op
  codexFallback: codex_desktop
  codexCliVersion: 0.147.0
  codexDesktopVersion: 1.2026.0628
  claudeCliVersion: 2.1.220
  # anthropicBeta: "claude-code-20250219,interleaved-thinking-2025-05-14,…"  # anthropic-beta header (claude_code_messages)
  opencodeVersion: 1.19.0
  opencodeClient: cli
  opencodeProject: global
  # opencode Zen free tier (needs profile: opencode_zen and a free model):
  opencodeZenFree: false        # /zen → /inference + bash/read tool stubs
  claudeSystemBlock: identity    # identity | billing
  extraHeaders: {}              # merged onto every profile
  urlRewrites:
    /v1/messages:
      appendQuery: beta=true    # servers that strictly check ?beta=true

Point your provider's own base URL at the gateway as usual — this plugin never touches provider configuration; it intercepts the requests that provider makes.

Configuration page

The page is a section of the Settings panel: Settings → Custom header, a nav row of its own beside General / Models / Plugins, registered into the settings.section slot.

The page is a nav rail plus three stable pages (the shell follows dsh-any-background's settings section). Every field has exactly one home, and switching pages never reclassifies a row — a field that relocated while being edited was the exact weirdness this layout removed:

pagefields
Identity & gatewaygatewayHosts, codexFallback, claudeSystemBlock
Version headersclaudeCliVersion, codexCliVersion, codexDesktopVersion, anthropicBeta, opencodeVersion, opencodeClient, opencodeProject, opencodeZenFree
AdvancedextraHeaders, urlRewrites

The identity page opens on a scenario picker, not on a list of fields. Accurate field names describe the config; they do not describe the job. Nobody arrives wanting to set gatewayHosts — they arrive wanting Codex, or Claude Code, or opencode, or the pi agent, to reach their own gateway. Each tab is one of those intents, and picking it stages the profile that makes the intent real:

scenariostages profile
Autoauto
Codexcodex_official / codex_tui / codex_desktop / codex_claude_plugin (a second-level choice)
Claudeclaude_code_messages
pipi_agent
opencodeopencode_zen
Offoff

Only the active page's fields are rendered, so one field is mounted exactly once and each control's DOM id stays unique.

Controls

Each field is drawn with the host control its shape calls for:

field shapecontrol
one-line textSettingsValueFielddraws its own label, overridden badge, reset, invalid copy and help region
2–4 fixed valuesSegmentedControlclaudeSystemBlock, codexFallback, the Codex variant
booleanSwitchopencodeZenFree
string list / JSON objecttextareathe page's own — the host ships no multiline row

The long explanations live behind each row's ? button rather than under it, which is what lets the always-visible hint stay one line.

The same form is registered a second time on the Plugins page, on the bundle's own row (Plugins → dsh-custom-header, the row's Configure control), through plugins.row.config. That is where the host's slot contract puts a third party bundle's configuration; the Settings section is where a user actually goes looking for it.

Nothing is written as you type: the page stages a draft and Save is the only thing that mutates the settings document, revision-fenced against a concurrent change elsewhere. Picking a scenario is one more staged edit and follows the same rule — it lands on Save and Discard undoes it. A save lands in the profile patch and takes effect on the next request — no restart.

Four consequences of the host contract are worth knowing if you touch this code:

  • The host half must export names only — never export default. The loader picks a plugin module off with unwrapExports (cordis-plugin-loader/lib/index.js), which prefers default over the namespace:

    exports = exports.default ?? exports      // default REPLACES the namespace
    if (!exports.__esModule) return exports
    return exports.default ?? exports
    

    so export default apply hands the registry a bare function: fiber.runtime is that function, therefore fiber.runtime.Config is undefined. dsh-settings reads the schema from exactly there (schema(entry) { const schema = entry.fiber?.runtime?.Config; … }) and bails out with return [] when it is missing — so no settings namespace is ever published. The symptom is maximally misleading: the plugin loads, its client half still registers the nav row, every log line is clean, and only the page body says "this host does not serve this configuration" — which reads like a client bug. Every shipped host half exports named only (dsh-client-ui-settings → export { Config, apply }; dsh-ops likewise); so does this one. build.mjs asserts the absence of a default export after every build.

  • The plugin needed no plugins.item entry. That slot is occupied by the official settings pages; a bundle's configuration belongs in plugins.bundle.config or plugins.row.config, keyed by the bundle's package name (here <package>#<row id> = dsh-custom-header#dsh-custom-header).

  • A settings.section registration carries the nav identity in its options: id (required — a fresh id is added beside the shipped rows, reusing theirs replaces that row), order (ascending position; the shipped rows sit at −10/0/10/15/20 and dsh-market at 40, so this one takes 50) and label (a thunk, re-read on every projection so it follows the locale). The Settings content column draws no heading for a section, so the page renders its own.

  • A .volatile() field is what makes the entry appear in the settings plane at all. Removing the marker removes the page, silently.

ctx.dshCustomHeader.setProfile('codex_desktop')   // programmatic runtime switch
ctx.dshCustomHeader.status()                      // diagnostics snapshot

Known residuals & limits

  • accept-language / sec-fetch-mode — injected by Node undici below fetch, outside the interceptor. Most servers only check UA + custom headers; terminate the rest at a local reverse proxy if needed.
  • ?beta=true for Anthropic — solved via urlRewrites.appendQuery, no proxy needed.
  • Cloudflare blocks — HTML edge responses cannot be fixed by header modification; switch API entry, ask for an allowlist, or forward locally. The llm/stream observer logs a 403 hint distinguishing CF HTML from JSON client-restriction rejections.
  • opencode Zen free tier — header spoofing alone no longer passes. With profile: opencode_zen, opencodeZenFree: true and a free model (*-free / big-pickle / grok-code), the body-patch middleware rewrites /zen → /inference and offers the bash/read tool stubs Zen now requires. The model id is read from the request body, which is why the URL rewrite lives in that middleware rather than in url-rewrite.ts.
  • Request body patches require a re-readable body — the body-patch middleware buffers JSON request bodies (content-length is re-computed); non-JSON or streamed request bodies are passed through untouched.

Development

npm install
npm run typecheck            # tsc against stubs/, not node_modules — see below
npm run build                # esbuild → lib/index.js + lib/client.js; tsc → lib/types (declarations)
npm run check                # typecheck + build
node tests/smoke.mjs         # local capture server; 9 scenarios, 31 checks

tests/smoke.mjs can also test a bundle without touching lib/ (useful while a live desktop app watches the mounted plugin):

npx esbuild src/index.ts --bundle --format=esm --platform=node --target=node22 \
  --external:@deepseek-ai/cordis --external:@deepseek-ai/dsh-* \
  --outfile=%TEMP%/dch-smoke/index.js
set SMOKE_LIB=%TEMP%/dch-smoke/index.js && node tests/smoke.mjs

@deepseek-ai/* comes from two other places, so the copies in node_modules/ are never the ones that matter:

  • types — tsconfig.json maps @deepseek-ai/* to stubs/@deepseek-ai/*. Those stubs are deliberately permissive (declare const z: { [key: string]: any }), so npm run typecheck validates this repo's code, not the host API surface. A stale local install therefore cannot break the build — and equally, a green typecheck does not prove the host's runtime supports what you called.

  • runtime — build.mjs keeps @deepseek-ai/dsh-* and @deepseek-ai/cordis external, so the plugin reaches the host's copies, not this repo's. That is demonstrable rather than assumed: @deepseek-ai/schemastery@3.18.1 has no volatile member at all (typeof z.string().volatile === 'undefined'), yet every field in config.ts calls .volatile() and the plugin loads cleanly. The host carries:

    specifierwhere the host keeps it
    @deepseek-ai/cordisapp.asar → 4.0.5-alpha.1
    @deepseek-ai/dsh-llmapp.asar → 0.2.1-alpha.2
    @deepseek-ai/schemasteryapp.asar → 3.18.5-alpha.1, and also profiles/<name>/node_modules → 3.18.4

    Both schemastery copies have .volatile(); this repo's does not. So the API surface you may rely on is the host's, and it drifts with the host — probe it rather than trusting the local install. Anyone importing lib/index.js directly (an offline self-check, say) hits the reverse problem: bare @deepseek-ai/* resolves to this repo's stale copies and blows up for reasons that have nothing to do with the code. Redirect those specifiers to the host's copies first.

License

MIT. Client-identity presets mirror publicly documented upstream formats; no third-party source is vendored. The fetch pipeline is vendored from @aizigao/pi-fetch-pipeline (MIT), re-keyed for DSH.