This is not a containment boundary. The plugin runs in-process, in the agent’s own process,
at the agent’s own uid. Anything the agent can execute — a bash command, a run_code
program, a mounted MCP server — can read every file the guard denies and can open its own
sockets without the plugin seeing anything. The guard closes the path where the model asks
for credential material through a tool. It does not stop code that is already running.
If you need containment, that is the sandbox, landlock-run, filesystem permissions, and
egress firewalling. Use this alongside them, not instead of them.
More limits worth stating up front:
tools/pre-execute listener that returns without calling next()
disables the breadth tier, and a tools/post-execute listener ahead of ours can replace a
result after it was redacted. ctx.tools.guard() is order-independent only because it has no
allow arm. A tools/pre-execute deny also skips guards entirely, so the audit sink cannot
claim to have seen every call.bash command line is split on
shell-ish separators and each token is tested as a path. That catches an unobfuscated
cat ~/.ssh/id_rsa. It catches nothing that tries: cat ~/.netr? (one glob character),
cat ~/.s""sh/id_r""sa, find ~ -name 'id_*' -exec cat {} +, a $(printf ...)
reassembly, a base64 round-trip of the path, or python3 -c opening the file — every one
of those was verified to read the file with the guard abstaining. Do not count this arm
as a control. A shell command is a program, not a path, and the only way to decide what
it will open is to run it. If the agent has a shell, credential files need filesystem
permissions or a sandbox, not this plugin.llm/stream the options are deep-frozen and next() takes no arguments, so a request the
agent has assembled goes out as it stands and a secret already in the conversation reaches
the provider. (The same waterfall’s response side is writable, and that is where remote
image destinations are neutralised — see below.) That is not the whole rule, though: agent/pre-step is an async waterfall
returning { kind: 'enter'; messages }, and the only production append of user/message
happens after it, so a message arriving from outside can still be rewritten before it is
logged or presented. This release does not do that; it is recorded here because the earlier
flat claim that outbound redaction is impossible was too strong.ctx.shellEnv rebuilds a
trusted DSH_* namespace for every model shell call, which is a way to hand bash and
pwsh — and only those two — the real value behind a placeholder without the model ever
seeing it. Planned work, not implemented here.non_interactive,
approval_mode and an apply beside a pending approvalPolicy are the three published
shapes; a tool that skips its confirmation under some other argument name is not covered, and
the registry is open, so this list is a floor on what is known rather than a description of
what exists.additionalContexts are not scanned. They are model-visible UserMessage payloads and
this release does not redact them.write or edit into a synced directory moves data off
the machine without going through an egress-capable tool.session-telemetry/record waterfall is not covered.$DSH_HOME is readable by a read-only tool. Profile manifests and the installed plugin
tree are ordinary work to read, so which plugins a profile loads is model-visible. Only writes
are denied wholesale there, plus reads of the credential material inside it.