docs(bash): reframe stdin/env — the scrub is the security control, not a trust boundary

Address review: the "trusted-plugin surface" framing overstated the security
story. A model driving the `bash` tool already has equivalent power to set env
vars and feed stdin through ordinary shell syntax (`FOO=bar cmd`, heredocs), so
the `env`/`stdin` seam fields grant it no new capability — and they cannot
exfiltrate the harness's ambient credentials, because the credential SCRUB in
dsh-bash-local (which strips *KEY*/*SECRET*/*TOKEN* from process.env before the
child sees it) is the actual control, and it works regardless of these fields
(tool-call args are static JSON, never shell-evaluated).

So drop the "dangerous / trusted-plugin boundary" language across the RFC, the
three bash-package READMEs, the bash/src/types.ts JSDoc, and docs/bash.md (both
the type-equiv blocks — kept 1:1 with source — and the prose). The reality that
remains: the `bash` tool doesn't EXPOSE env/stdin as parameters because they'd
be redundant with shell syntax; the fields exist for in-process plugins (the
hooks bridges) to pass a JSON payload + CLAUDE_* vars cleanly. The guard test is
kept but reframed: it catches a future `...args` spread that would silently
forward model input into the post-scrub env merge, NOT a trust wall. No code or
behavior change.
This commit is contained in:
Tianyi Cui
2026-07-02 04:08:24 +08:00
parent 318a3fd036
commit bb9ae2ba99
7 changed files with 52 additions and 46 deletions
+11 -11
View File
@@ -47,19 +47,20 @@ export interface BashExecRequest {
signal?: AbortSignal | undefined
/**
* Bytes to write to the command's stdin, then close it. Absent leaves stdin
* closed/empty (the default for model-driven tool calls). A TRUSTED-PLUGIN
* surface: the model-facing bash tool does NOT thread model-supplied input
* here — it is set by in-process plugins (e.g. the hooks bridges, which write
* a hook command's JSON payload to its stdin).
* closed/empty (the default for model-driven tool calls). Set by in-process
* plugins (e.g. the hooks bridges, which write a hook command's JSON payload
* to its stdin); the model-facing bash tool does not expose it as a parameter
* (a model that needs stdin uses shell syntax like a heredoc or a pipe).
*/
stdin?: string | undefined
/**
* Extra environment entries for the command, merged AFTER the
* implementation's credential scrub (so an explicit entry here is honored even
* when its name matches the scrub pattern — the caller takes responsibility).
* Like {@link stdin}, a TRUSTED-PLUGIN surface: the model-facing bash tool
* never forwards model-supplied env; in-process plugins (the hooks bridges)
* set hook env vars (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, …) here.
* when its name matches the scrub pattern — the caller named a value it holds,
* not the harness's ambient secret). Set by in-process plugins (the hooks
* bridges set `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, …); the model-facing
* bash tool does not expose it as a parameter (a model that needs an env var
* uses shell syntax like `FOO=bar cmd`).
*/
env?: Record<string, string> | undefined
/**
@@ -92,8 +93,7 @@ export interface BashExecSpec {
* verbatim from {@link BashExecRequest.stdin}. OPTIONAL on the resolved spec
* (unlike `owner`): it has no config default, so a missing one means "no
* stdin" — the safe, ordinary case — not a silent footgun, so it stays a
* plain optional rather than required-but-nullable. A TRUSTED-PLUGIN surface
* (see the request field).
* plain optional rather than required-but-nullable (see the request field).
*/
stdin?: string | undefined
/**
@@ -101,7 +101,7 @@ export interface BashExecSpec {
* {@link BashExecRequest.env} and merged by the implementation AFTER its
* credential scrub (an explicit entry wins even when its name matches the
* scrub pattern). OPTIONAL on the spec for the same reason as `stdin` — no
* config default, absent means "no extra env". A TRUSTED-PLUGIN surface.
* config default, absent means "no extra env".
*/
env?: Record<string, string> | undefined
/**