sleep-not-permitted
Manifest validRejects foreground waiting in DSH agents: waiting commands (sleep, wait, tail -f, watch, ping without -c, infinite loops), blocking wait tools (wait_agent, job_output with wait: true), progress pollin
sleep-not-permitted
中文 · English
A DeepSeek Harness (DSH) plugin that forbids
foreground waiting. When an agent tries to run a command whose purpose is waiting
(sleep, wait, tail -f, watch, ping without -c, infinite loops), or calls a blocking
wait tool (wait_agent, job_output with wait: true), the call is rejected — and the rejection
is a set of instructions: run it in the background (run_in_background: true), or end the turn and
let the completion notice wake you.
Nothing is silently rewritten: DSH logs and shows tool arguments before dispatch, so a guard can only refuse, not impersonate a different call. The plugin refuses, explains, and gets out of the way.
Why a plugin, and not AGENTS.md or a preset
- AGENTS.md is per-workspace. You would have to write the rule into every workspace, and every new workspace silently misses it.
- A preset is per-agent configuration. Every composition needs its own copy, and the user has to pick it.
- A host plugin is profile-level. Install once and every workspace, session, and subagent is covered. It can carry configuration, be upgraded, disabled, and removed.
Why it is needed
A DSH session moves one step at a time: however long a foreground tool call runs, the session stops
with it. bash({ command: "sleep 60" }) looks harmless but freezes the conversation for a minute,
and a five-minute wait_agent freezes it for five. DSH has a safety net for slow work (a
foreground command that exceeds its timeout is promoted to a background job), but that still burns
the timeout first — and waiting is not work at all.
So the plugin does two things:
- Prevention — a rule in the system prompt, next to the existing background-jobs section, so the model picks the background form first;
- Enforcement — a synchronous
ctx.tools.guard()denial when the foreground form is used anyway, with the retry spelled out.
What gets blocked
- Waiting commands in
bash/pwsh/terminal_send:sleep,wait,tail -f,watch,pingwithout-c,yes, infinite loops, log-follow modes — see the rule table below. - Blocking wait tools:
wait_agentandjob_outputwithwait: true, when the requested wait exceeds the budget (default 30 seconds). - Polling: running the same check command more than
pollRepeatThresholdtimes withinpollWindowMs— the "wait a little, check, wait a little, check" substitute for sleeping. It is refused with the same instruction: wrap the waiting itself in a background job. - Commands that need human input or confirmation:
sudowithout-n,sshwithout-o BatchMode=yes, login/key-generation flows (npm login,gh auth login,ssh-keygen, …), interactive editors and pagers (vim,less,top, …),docker run/exec -it,read -p,rm/cp/mv -i. They are refused in the foreground and in the background: a background job has no stdin, so it would hang silently — never settling, never notifying.
If what you are waiting for has no DSH job handle (a remote host, an external service), be it a
command or a poll: wrap the waiting itself in a background job — for example
ssh host 'until [ -f done ]; do sleep 5; done' with run_in_background: true (sleep is allowed
inside a background job) — then end the turn; that job's completion wakes the agent. This is the
correct replacement for both sleep and polling when there is nothing local to await.
Behavior
- A foreground wait is rejected with
Error: <reason>; the reason contains the exact retry form. - A call that already has
run_in_background: truepasses — that is the shape this plugin wants. - Ordinary commands (
git status,npm test,curl,grep, …) pass untouched. ask_user_questionis not a wait — it is you being asked — and is never blocked.- Non-blocking reads (
job_outputwithoutwait,list_agents,team_task_list) pass.
The default budget (30 s) exists so short coordination waits keep working out of the box (DSH's
own Agent Teams guidance has the Lead wait for teammates). Waits above the budget — the five-minute
wait_agent(timeout_ms: 300000) case — are rejected. Set maxBlockingWaitMs: 0 for strict mode:
not a single second of foreground waiting.
After a background job finishes
A completion notice means the job finished, not that it succeeded. The notice carries the
status (completed / failed / killed) plus the label; read the output and exit code with
job_output before continuing, and diagnose a failed/killed job instead of retrying blindly.
Background jobs have no timeout of their own. A job that dies mid-flight or hangs while waiting
for something that never happens never settles — and so never notifies. Bound it yourself: wrap the
command in timeout N, keep a deadline in retry loops, or write a marker/report file at the end.
For cross-machine waits the same applies to the remote wait loop: give it a maximum wait and let it
exit with a failure marker on timeout, so the local job always settles and always notifies.
If the work needs a human decision in the middle, do not background it: confirm first
(ask_user_question, or the DSH approval channel), then start it with non-interactive flags.
Rules
| id | matches |
|---|---|
sleep | sleep N in command position (sudo sleep, timeout 30 sleep, do sleep, && sleep, …) |
sleep-nested-shell | nested shell payloads such as bash -c "sleep 10" |
sleep-script | time.sleep(30), python -c "...sleep(5)" |
wait | the shell builtin wait |
tail-follow | tail -f / tail -F / tail --follow |
watch | watch <cmd> (re-runs forever) |
ping-forever | ping without -c |
yes | bare yes (endless output) |
infinite-loop | while true / while : / until false |
follow-logs | docker/podman logs -f, journalctl -f, kubectl logs -f |
Plain text inside quotes does not count (echo "sleep 30", grep 'sleep 30' file pass);
in multi-line scripts, command position is evaluated per line.
Configuration
Override the bundle patch row from the profile's cordis.patch.yml, or edit the plugin's
cordis.patch.yml:
- id: sleep-not-permitted
name: 'dsh-plugin-sleep-not-permitted'
config:
enabled: true # false = turn the whole guard off
tools: [bash, pwsh, terminal_send]
blockingTools: [wait_agent] # blocking wait tools: reject above the budget
blockingWaitFlagTools: [job_output] # reject when wait: true and above the budget
maxBlockingWaitMs: 30000 # 0 = reject every foreground wait
antiPolling: true # refuse repeated check commands (busy-poll)
pollRepeatThreshold: 3 # max times one command may appear inside the window
pollWindowMs: 120000 # polling window, milliseconds
interactiveGuard: true # refuse commands that wait for input/confirmation
disabledInteractiveRules: [] # switch off individual interactive rule ids
language: auto # auto | both | zh | en (message + prompt language)
announceInPrompt: true # false = enforcement only, no prompt rule
disabledRules: [] # switch off individual rule ids from the table
extraRules: # add your own rules; pattern is a regex string
- id: long-curl
pattern: 'curl\s+[^|;&]*--max-time\s+\d{3,}'
label: long-timeout curl
reason: This curl can hang for a long time — run it in the background.
Invalid configuration fails loud at mount time (unknown rule id, empty tools, invalid regex,
non-boolean switch, unknown language) — there is no "installed but silently inactive" state.
Message language
language: auto (the default) follows the user's system language: it checks DSH_LOCALE,
LC_ALL / LC_MESSAGES / LANG, then the macOS system language preference, then Node's Intl
default, and falls back to English. A Chinese system gets Chinese-only messages and a
Chinese-only prompt section (roughly half the prompt cost); both emits both languages; zh /
en pin one explicitly. Resolution happens once at mount.
Install / uninstall
The package is a standard DSH bundle (dsh.bundle.patch → cordis.patch.yml).
git clone https://github.com/JJJACKsong/sleep-not-permitted.git
# desktop profile (managed by the Electron app): use plugin_manager install_bundle
# with the absolute path of the cloned directory
# other profiles:
dsh plugin --profile web add /path/to/sleep-not-permitted
Prebuilt tarball (no clone, no build):
dsh plugin --profile web add https://github.com/JJJACKsong/sleep-not-permitted/releases/download/v0.1.0/dsh-plugin-sleep-not-permitted-0.1.0.tgz
Uninstall:
dsh plugin --profile web remove dsh-plugin-sleep-not-permitted
# desktop: plugin_manager remove_bundle dsh-plugin-sleep-not-permitted
Disable without uninstalling: set enabled: false, or toggle the sleep-not-permitted row in the
plugin manager.
Tests
node --test test/detect.test.mjs test/plugin.test.mjs
test/detect.test.mjs— detector positives and negatives (command position, quotes, nested payloads, false-positive protection);test/plugin.test.mjs— a minimal ctx double checks deny/pass, defaults, strict mode, message language, and fail-loud configuration.
Verify it works
- Foreground
sleep 5→ rejected immediately withError: …; wait_agent(timeout_ms: 300000)→ rejected (budget 30 s);- Background
sleep 5(run_in_background: true) → returns a job id at once, notify on completion; - The system prompt contains the rule block (
announceInPrompt: true).
Compatibility
Tested against DSH 0.1.5-rc.2 and 0.2.1-rc.3. It relies on two host seams —
ctx.tools.guard() and ctx.systemPrompt.section() — and fails loud at mount if they change.
Boundaries
- Does not guess "might be slow".
npm install,cargo buildand other commands that make progress still run in the foreground; the tool timeout promotes them to a background job (default 120 s, capped at 600 s). - Does not block work. Long synchronous tasks such as
workflowordeliver_gateare not waits and are left alone. - Does not rewrite arguments. A denied call costs one retry round trip; the system-prompt rule is there to keep that rare.
- Does not inspect script files. How long
bash script.shsleeps inside cannot be judged statically; the tool timeout is the backstop.
Compatibility
Versions
| Latest version | Published | Size |
|---|---|---|
| 0.4.0 | — | — |
Comments
Loading…
Similar plugins
by john-walks-slow
wait_subagent tool for DeepSeek Harness: block until a background continuable subagent settles and return its stop reason and closing message — event-driven, zero polling.
★ 1
MIT
JavaScript
Oct 26, 2026
dsh plugin --profile web add dsh-wait-subagentby cxv117793-blip
Counts agent steps and, every N of them, adds a progress-report instruction to the batch that is about to be sent, so the agent states what it is doing, what it finished and what comes next, then carr
★ 4
MIT
JavaScript
Oct 30, 2026
dsh plugin --profile web add dsh-step-pauseby Kaiji-Z
An RTS-style orchestration dashboard where commands replace chats: a resident commander agent decomposes each order into task briefs with acceptance criteria, typed subagent troops (recon/engineer/med
★ 3
↓ 420/wk
MIT
TypeScript
Sep 30, 2026
dsh plugin --profile web add dsh-plugin-stardeckby Martlet-Tech
Explain what a pending dsh approval will actually do, with one LLM call: an Explain button beside Allow/Reject in the DeepSeek Harness Web GUI.
★ 2
MIT
JavaScript
Sep 22, 2026
dsh plugin --profile web add dsh-approval-explainby yaopushen
Antigravity-style run_command for DSH: short commands return inline within a configurable wait window (default 10s) while longer ones auto-promote into ctx.jobs for later collection and completion not
★ 1
MIT
TypeScript
Oct 25, 2026
dsh plugin --profile web add dsh-plugin-background-tasksby kiterunner1
DSH 插件:任务结束或命令卡死时用本机人声提醒,自动中断卡住的 pwsh 调用并把「怎么继续」写回工具结果,并在 spawn 之前拦掉已知的性能陷阱写法。仅 Windows。 / Beeps + stuck-command watchdog + command guard for DeepSeek Harness. Windows only.
★ 0
MIT
JavaScript
Sep 15, 2026
dsh plugin --profile web add dsh-attention-beep