Files
deepseek-harness/packages/tool-bash

@deepseek-ai/dsh-tool-bash

The model-facing bash tools — bash, bash_output, bash_kill — registered over the ctx.bash executor seam (@deepseek-ai/dsh-bash). Pure schema + text shaping; every process concern lives behind the seam, so sandboxed or remote executor implementations swap in without changing what the model sees.

Requires a loaded executor implementation (e.g. @deepseek-ai/dsh-bash-local); the plugin stays pending until ctx.bash exists (inject: ['tools', 'bash']).

Tools

bash

Arg Type Notes
command string (required) Run via bash -c. No state persists between calls — use workdir, not cd.
description string (required) One-line, active-voice summary of the command (5-10 words), for UI/log display only — no effect on execution.
timeoutMs number Timeout override in milliseconds. The executor applies its configured default and cap.
workdir string Working directory for this call. Defaults to the calling agent's session cwd (session.header.cwd) so each session runs in its own workspace; a relative workdir is resolved against that session cwd.
run_in_background boolean Return a task id immediately; no timeout applies.

command, workdir, and timeoutMs are resolved against the executor's config defaults via ctx.bash.resolve() before execution, so the executor seam (BashExecSpec) receives explicit workdir/timeoutMs values. The workdir default is applied in the tool layer (from the calling agent's session.header.cwd) BEFORE resolve() — the per-session cwd must come from exec.agent, since N sessions share one executor; only when no session cwd is available does the executor fall back to its own config / process.cwd().

Result text: stdout, then a [stderr] section, then status markers — [timed out after Nms] whenever the executor's timer fired (reported independently of how the process ended, so a command that traps SIGTERM and exits 0 still shows it), [killed by signal: …] for a signal death, [exit code: N] for a non-zero exit (reported, not isError: the model decides how to react), and [output truncated; full output: <path>] when the tail was kept. Only infrastructure failures (spawn errors, aborts) surface as isError results.

bash_output

task_id → output produced since the previous bash_output call plus a status line (running / completed, exit code: N / killed). Reads that lost data to buffer bounds say so and point at the full-output spill file.

bash_kill

task_id → ask the executor to kill the background task. The concrete executor decides how to signal or stop the process; killing an already-finished task is a reported no-op, and unknown ids are errors.

Task ownership (cross-session isolation)

The owning agent is recorded per task id at spawn and kept for the lifetime of the loaded plugin instance (it is not cleared on completion). bash_output/bash_kill reject a task owned by a different agent with task <id> belongs to another session (a task started with no agent — a non-loop caller — has no owner and is open to anyone; a call with no exec.agent cannot access an owned task). Task ids are global and predictable, so under multi-session ACP this ownership check is the fence that stops one session's agent from reading or killing another session's background task. (TODO(tool-bash-owner-hmr): an independent HMR reload of this plugin starts a fresh map, so a task spawned before the reload becomes un-owned — acceptable as HMR is dev-only and the session boundary is one user's cooperative editor; a durable fix attaches ownership to the executor/task lifetime.)

Background completion notices

When a background task finishes, a short notice is injected into the owning agent's session (agent.inject(), source {kind: 'plugin', plugin: 'tool-bash'}). Injection is durable context for the next model request, not a wake-up — an idle agent stays idle until something sends a message. That's why the tool descriptions tell the model to poll with bash_output.

Permissions

TODO(permissions): commands run with the executor's full authority. The permission/sandbox seam is the tools/execute waterfall (veto or ask) plus sandboxing BashExecutor implementations — see docs/architecture.md. @cordisjs/plugin-capability (a named-permission service with a session test()) is a candidate building block for that work.