dsh-blocker-notify

August 14, 2026 · View on GitHub

English | 简体中文

Real-time attention alerts for DeepSeek Harness (DSH): when an agent gets stuck and needs you to step in, the web UI notifies you in real time — no need to watch the screen waiting for an action.

示例

The three kinds of "blocked" signals that trigger a notification:

SignalDescription
Approval / privilege escalation (approval/request)The agent requests wider sandbox permissions or an approval (e.g. a write escalation under read-only mode) → alerted
Sandbox denial (FS_SANDBOX_DENIED)write/edit refused under read-only / workspace-write mode → recorded in the host log only (diagnostics), no user-facing alert
Waiting for user interaction (question / plan-review)The agent is waiting for you to answer a question or review a plan → alerted

Only events that need your action are alerted (approval / question / plan review). A sandbox denial is a completed event with nothing to act on, so it stays silent — even when the approval policy is never.

The four notification mechanisms

MechanismEffect
Global message (banner)An amber banner in the top-right corner lists every blocked session; each row shows only the session title + state (no tool name / reason); clicking a row jumps to the session and removes that row immediately (independent of the approval outcome); ✕ closes the whole banner; rows reappear when new blockers arrive
Workspace entry turns yellow and flashesThe yellow status dot on the affected session row in the left workspace flashes every 1.1s — the yellow mark itself is DSH's own "waiting for user" indicator; this plugin adds the flash animation; it stops automatically once the blocker is resolved
Sound (audible chime)When a new blocker appears, the page plays a short two-tone chime (Web Audio, no audio files); it only rings on blocker-set transitions, never on page load with pre-existing blockers; degrades silently when autoplay is locked or the environment lacks Web Audio
OS-level notificationWhen a new blocker appears, the browser raises an OS notification (Windows Action Center / macOS Notification Center) via the Notification API; clicking the notification focuses DSH (and opens the session directly for a single blocker); one blocker → body session title — kind (tool name — reason), several blockers → one merged notification; when permission is granted the banner header shows a small 🔔 (click for a self-test notification), while default/denied states show an action row in the banner (one-click enable / fix in browser site settings)

How to use

Nothing to install, no DSH restart needed. Downside: process-memory only — re-enable after a DSH restart.

  1. Get the source: git clone https://github.com/Frost-Reed/blocker-notify (or download from the GitHub page).

  2. Open any session (preferably cordis / creator mode) and send this instruction to the agent (replace <repo-path> with your local clone path):

    Please enable dsh-blocker-notify as a dynamic plugin:
    1. Use the read tool to read <repo-path>/dynamic/host.js and <repo-path>/dynamic/client.js (these are the code.host and code.client function bodies — use the file contents directly).
    2. Call cordis_define: plugin: { kind: "new", idPrefix: "ntfy" }; name: "blocker-notify"; purpose: one sentence; code.host = host.js content; code.client = client.js content.
    3. Call cordis_run (mode: "run") with the pluginId/packageId returned by cordis_define.
    4. The run may require the user to approve the Client half in the web UI; wait for the approval result, do not re-request.
    5. When done, use cordis_inspect_self(pluginId, packageId) to confirm host/client are both running.
    
  3. Approve the Client half's run request in the web UI (once).

  4. Verify immediately (see "Verification" below).

Option 2: Formal bundle install (persistent, v0.2.0)

Packaged per DSH's official plugin spec (dsh.bundle + dsh.client); survives restarts once installed.

If dsh is not on your PATH (e.g. running from a source checkout or not installed globally), replace dsh below with pnpm dsh (in the source repo root) or npx @deepseek-ai/dsh.

Install from GitHub (published, recommended):

dsh plugin --profile web add github:Frost-Reed/blocker-notify

Install from a local checkout (development):

dsh plugin --profile web add file:<local-clone-path>

After installing:

# Confirm it joined the layer stack
Get-Content $env:USERPROFILE\.dsh\profiles\web\package.json   # dsh-blocker-notify should appear in dsh.profile.bundles
# Confirm the row is in the config tree
dsh --dump-config --profile web | Select-String "dsh-blocker-notify"
# New dependencies/layers require a process restart
dsh web

lib/ is committed build output (generated from src/ by tsdown). Git installs run the prepare build script, so pnpm needs a one-time allowBuilds authorization; alternatively install the npm package or a pnpm pack tarball (prebuilt, no authorization needed).

Verification (common to both options)

ScenarioActionExpected
Question self-testAsk the agent to call ask_user_question onceWhile the question dialog is open: banner "等待回答" + chime + OS notification; clicking the banner row jumps and removes it
Approval / escalationIn settings set the approval policy to ask and the sandbox to read-only, then have the agent attempt a write with sandbox_permissionsWhen the approval card appears: banner "等待授权" + chime + OS notification (with tool name and reason); clicking the banner row jumps and removes it (whether or not the approval is granted)
Sandbox denialHave the agent write to a denied path without escalation argumentsSilent (no banner, no chime, no OS notification; the host log records it for diagnostics only)

How it works

Host half (in-process, untagged scope → observes every agent's approvals/tool results)
  ├─ ctx.on('approval/request', (req, next) => …)   // waterfall: observe only, must return next()
  └─ ctx.on('tools/result', …)                       // detects FS_SANDBOX_DENIED
        │  writes to the in-process alert log (deduped by key, 10-min TTL, cap 60)

  Details channel: formal = webServer route GET /api/dsh-blocker-notify/alerts
                   dynamic = harness.handle('blocker-notify/alerts') RPC

Client half (browser page, root scope)
  ├─ useSessions (shell.overlay standard prop) → live pendingInteraction (approval / plan-review / question)
  ├─ polls details (fetch / host.call) → merges approval details (tool name / reason)
  ├─ CSS injection → flashes [data-state="warning"] dots
  ├─ Web Audio → plays a chime when an actionable blocker appears (AudioContext primed on first interaction)
  └─ Notification API → raises an OS notification for actionable blockers (one-click enable + self-test in the banner; clicking focuses the page)
  • Host half: captures blocking signals and maintains the in-process alert log (plain JS, no UI).
  • Client half: the four notification channels. Data comes from the shell.overlay standard prop useSessions (snapshot { ids, byId, … }; byId entries carry id / title / pendingInteraction) plus the Host details poll.
  • The formal and dynamic versions share the same logic; only the "details channel" differs (webServer route vs. dynamic RPC) — see docs/ROUTE-B.md.

FAQ

SymptomFix
No notification at allConfirm a blocker actually exists (question/approval/sandbox denial); for the dynamic version confirm cordis_inspect_self shows host/client running; refresh the page and retry
Flash only, no bannerThe banner reads the { ids, byId } snapshot from useSessions — make sure you are on pkg-4 / v0.2.0 or later
Banner only, no flashThe flash is the CSS animation (span[data-state=warning]) — make sure you are on pkg-3 / v0.2.0 or later
Dynamic version gone after restartExpected — dynamic plugins are process-memory; switch to Option 2 (formal install)
Formal install not visible in the GUINew dependencies/layers require a dsh web restart; the plugin list is in Settings — reopen to refresh
First dsh plugin add github:... failsThe package has a prepare build script (tsdown), so pnpm needs authorization: copy the exact package key from the error into allowBuilds in $DSH_HOME/profiles/web/pnpm-workspace.yaml and re-run; lib/ is committed, so an npm package or pnpm pack tarball installs without authorization

Development & build

  • src/ is TypeScript source (written the official source-repo way); lib/ is the tsdown build output (committed). pnpm build (tsdown, replicating the official clientBundle preset) generates lib/index.js (ESM named-export plugin) and lib/client.js (official window.__ModuleLoader__.load({ id, factory }) web bundle).
  • Committing lib/ means git installs work; the prepare script (= tsdown) rebuilds on git install.
  • Offline smoke test: node scripts/smoke.mjs (validates both halves' exports, inject, event listeners, route JSON, CSS injection, slot registration, rendering, UTF-8 copy).
  • dynamic/ holds the dynamic-plugin function bodies (used by Option 1), logically identical to lib/ with a different channel.
  • Full Route-B research and implementation record: docs/ROUTE-B.md; fresh-deployment enablement guide: docs/ENABLE.md.

Repository layout

dsh-blocker-notify/
├── LICENSE               # MIT
├── package.json          # bundle (dsh.bundle) + client (dsh.client) manifests
├── README.md             # Chinese readme
├── cordis.patch.yml      # bundle patch: inserts the Host row
├── src/                  # TypeScript source (official source-repo method)
│   ├── index.ts          # Host half (ESM name/inject/apply + webServer route)
│   └── client/index.ts   # Client half (built into the ModuleLoader web bundle)
├── tsdown.config.ts      # build config (replicates the official clientBundle preset)
├── tsconfig.json
├── lib/                  # tsdown build output (committed)
│   ├── index.js          # Host half
│   └── client.js         # Client half (ModuleLoader format)
├── scripts/
│   └── smoke.mjs         # offline smoke test
├── dynamic/              # dynamic-plugin function bodies (Option 1)
│   ├── host.js
│   └── client.js
└── docs/
    ├── ENABLE.md         # fresh-deployment enablement guide
    └── ROUTE-B.md        # Route-B formalization record

License

MIT