English | 中文
pyrun
Run Python in DeepSeek Harness with one tool call. The model writes the program;
pyrun hands it to python -X utf8 -u - on stdin — no shell quoting layer, no temporary
script, UTF-8 on both streams, under the same sandbox and approval rules as the built-in
pwsh tool.
python(code="""from datetime import date
print(f"你好 {date.today()}")""")
→ 你好 2026-11-05
What it deletes
| You need to | Built-in route today | With pyrun |
|---|---|---|
| Run a snippet | pwsh → python -c "…" → escape every quote |
one python call, source verbatim |
| Run a multi-line script | write a .py file, run it, delete it |
the same call, no file left behind |
| Get Chinese output back | GBK mangling on the way home | UTF-8 end to end, emoji included |
| Keep a program that worked | nothing: copy it out of the transcript by hand | save_to: 'scripts/report.py' writes exactly what ran |
| Run a ten-minute job | the call blocks until the timeout | run_in_background: true, collect with job_output |
| Write outside the workspace | denied, no way forward | one-shot sandbox_permissions retry with approval |
What it costs

Without a Python tool a program does not get a round of its own. It is pasted into a shell command and
the shell parses it first: python - <<'PY' is a redirection with no file, and a < inside a quoted
program is "reserved for future use". Of the 949 rounds this machine gave a program, 20% came back
an error — 41 because the shell read Python as PowerShell, 6 because the console ate the answer:
Windows printed [WinError 5] 拒绝访问。 and the transcript received �ܾ����ʡ�.
The retry is not free, because every request re-sends the whole conversation. Removing the repair attempts — and the leftovers they added to every later replay — saves 21% of what those ad-hoc runs cost, and 0.56% of everything, across the 43 sessions that predate this tool. In 9 of them the saving is big enough to notice: median 1.9%, worst 8.6%.
| a shell round | pyrun | |
|---|---|---|
| who parses the program | PowerShell, then Python | Python only |
| when it goes wrong | ParserError, or a message with the text eaten |
a traceback naming your own line |
| after a failure | the retry re-sends the prefix | the retry is the fix |
| rounds that disappear | 0.30% of all rounds | — |
| tokens | — | 21% of the ad-hoc work · 0.56% of the session |
Token savings are modest in absolute terms; what changes is how much a person has to watch — 14 rounds that no longer happen, 27 minutes of spinner, in a run whose answer arrives on the first try.
Measured on this project's own transcripts: 43 sessions with no python tool and 3,547 requests,
every one ending before this repository's first commit, plus the 949 shell rounds that carried a program
across all sessions. Billed tokens are the per-request sum of cacheReadTokens, inputTokens and
outputTokens; a removed attempt's additions are then subtracted from the prefix of every later request.
0.56% is that difference over all 3,547 requests; 21% measures the same difference against only the
tokens those ad-hoc runs consumed. Reproduce with tools/measure; the figure's source is
docs/cost-figure.html.
Install
dsh plugin --profile web add @ds-hu/dsh-plugin-pyrun
Then restart dsh: the host half is a Node module and is only re-read at startup.
The Plugins page in the Web UI installs the same package.
From a checkout, for development:
dsh plugin --profile web add link:D:/Projects/dsh-plugin-pyrun
Requires DSH 0.2.x and python on PATH — the plugin does not probe for an interpreter.
Swap web for the profile you actually run.
Use
Every call is one program.
| Field | What it is for |
|---|---|
code |
the program, executed verbatim through stdin |
workdir |
working directory; defaults to the session's current one, and a relative path resolves against it |
save_to |
save the source to a file before the run; atomic, parent directories created, fenced by the session's file policy |
timeout_ms |
foreground budget; rejected together with a background run |
run_in_background |
returns a job id at once; no timeout applies |
sandbox_permissions + justification |
one-shot retry of a call the sandbox just denied |
Output is stdout, then a [stderr] section, then markers: [saved to <path>], [exit code: N]
(a non-zero exit is a result, not a failure), [timed out after Nms], [killed by signal: …],
[output truncated; full output: <path>], [sandbox: file access denied under <mode> mode].
save_to writes through the session filesystem, so the session's file policy decides: refused
under read-only, confined to the workspace under workspace-write, and — in a deployment that
enables the read-before-write policy — refused when the target exists and this session has not
read it. The save happens before the program starts: a refused save never runs anything, and a
returned job id means the file is already on disk.
Background jobs appear in job_list with kind python, are read with job_output, and
stopped with job_kill.
Limits
- A foreground timeout kills the program; it never promotes to a background job. For long
work, pass
run_in_backgroundon the first call. - The program cannot read interactive input — stdin carries the source.
- A saved file is not the same execution environment:
python -runs withsys.path[0]at the cwd and__file__as<stdin>; running the same file from disk later changes both (imports of sibling modules start resolving). - No config surface: the executor's own defaults and caps decide timeouts and output limits.
More
Runtime contract, dependency placement, release process, and the "every tool call fails" troubleshooting live in AGENTS.md.
MIT
No comments yet. Be the first to write one.