Vulnerability
When a sub-agent is granted a vault secret (zeph-subagent grants system), the raw value is injected as a per-call ExecutionContext env override before a tool executes (crates/zeph-subagent/src/agent_loop.rs:684-694, deliver_secret in crates/zeph-subagent/src/manager/secrets.rs:87-112). If the tool's own output happens to include that value (e.g. a bash step that runs env, echoes the variable, or a verbose HTTP client that logs its Authorization header), the raw secret becomes part of the tool-result content (agent_loop.rs:709-717) with no secret-masking pass applied to it.
From there the unmasked value is:
- Written verbatim to the on-disk JSONL transcript (no scrubbing in
transcript.rs::append).
- Sent back to the LLM provider in the next turn's request (network egress to a third party).
- Captured by
--debug-dump when enabled (agent_loop.rs:186-223, dump_request/dump_response receive the full unmasked messages).
The crate already has an optional SecretMaskRegistry/PiiFilter layer (manager/mod.rs:395-405, forward.rs:125-139), but it is wired only into the TUI forwarding/display pipeline (maybe_spawn_forward) — deliver_secret never registers the delivered value into that registry, so even the forwarding path doesn't catch it. The ContentSanitizer passed into the loop (sanitizer.sanitize(...) at agent_loop.rs:769) is only invoked when a PostToolUse hook replaces the tool output, not on the raw exec_result content in the common (no-hook) case, and it targets injection-pattern detection, not secret-value masking. zeph-sanitizer (owner of SecretMaskRegistry) is already a dependency of zeph-subagent, so a fix is architecturally straightforward.
The main agent (zeph-core) has an analogous placeholder-masking scheme ("PAAC": unmask_json_value/prepare_tool_dispatch in tier_loop.rs) that keeps raw secrets out of LLM-facing context entirely — the sub-agent path has no equivalent protection.
Severity
Critical — a granted secret can reach the transcript file, the third-party LLM provider, and on-disk debug dumps in plaintext with no masking anywhere in the common path.
Location
crates/zeph-subagent/src/manager/secrets.rs:87-112 (deliver_secret — raw value delivered, never registered for masking)
crates/zeph-subagent/src/agent_loop.rs:684-798 (secret injected into tool env; tool output pushed into messages unmasked)
crates/zeph-subagent/src/agent_loop.rs:186-223 (debug dump receives unmasked messages)
Attack Scenario
A sub-agent is granted a vault secret (e.g. ZEPH_TELEGRAM_BOT_TOKEN) scoped to a specific tool call. The sub-agent (whether through a legitimate but overly-verbose tool, a buggy skill, or a prompt-injected instruction from untrusted tool output earlier in its own turn) runs a bash command like env | grep ZEPH or a curl command with -v that echoes headers. The raw secret value appears in the tool's stdout, gets wrapped into content unmodified, and is: (1) persisted forever in the transcript JSONL, (2) sent to the external LLM provider as part of the next request, and (3) written to disk in --debug-dump output if enabled. Any of these three sinks constitutes a credential leak outside the intended trust boundary.
Remediation
Register every value delivered by deliver_secret into SecretMaskRegistry (or equivalent) at grant time, and apply that registry's masking pass to tool-result content unconditionally (not just on the hook-replacement branch) before it is pushed into messages, written to the transcript, or handed to the debug-dump sink — mirroring the zeph-core PAAC placeholder-masking approach used for the top-level agent loop.
References
Related: #6460, #6461, #6453 (recent transcript/vault-anchor integrity hardening — this finding is a distinct gap, secret value exposure rather than transcript-tampering)
Vulnerability
When a sub-agent is granted a vault secret (
zeph-subagentgrants system), the raw value is injected as a per-callExecutionContextenv override before a tool executes (crates/zeph-subagent/src/agent_loop.rs:684-694,deliver_secretincrates/zeph-subagent/src/manager/secrets.rs:87-112). If the tool's own output happens to include that value (e.g. a bash step that runsenv, echoes the variable, or a verbose HTTP client that logs itsAuthorizationheader), the raw secret becomes part of the tool-resultcontent(agent_loop.rs:709-717) with no secret-masking pass applied to it.From there the unmasked value is:
transcript.rs::append).--debug-dumpwhen enabled (agent_loop.rs:186-223,dump_request/dump_responsereceive the full unmaskedmessages).The crate already has an optional
SecretMaskRegistry/PiiFilterlayer (manager/mod.rs:395-405,forward.rs:125-139), but it is wired only into the TUI forwarding/display pipeline (maybe_spawn_forward) —deliver_secretnever registers the delivered value into that registry, so even the forwarding path doesn't catch it. TheContentSanitizerpassed into the loop (sanitizer.sanitize(...)atagent_loop.rs:769) is only invoked when aPostToolUsehook replaces the tool output, not on the rawexec_resultcontent in the common (no-hook) case, and it targets injection-pattern detection, not secret-value masking.zeph-sanitizer(owner ofSecretMaskRegistry) is already a dependency ofzeph-subagent, so a fix is architecturally straightforward.The main agent (
zeph-core) has an analogous placeholder-masking scheme ("PAAC":unmask_json_value/prepare_tool_dispatchintier_loop.rs) that keeps raw secrets out of LLM-facing context entirely — the sub-agent path has no equivalent protection.Severity
Critical — a granted secret can reach the transcript file, the third-party LLM provider, and on-disk debug dumps in plaintext with no masking anywhere in the common path.
Location
crates/zeph-subagent/src/manager/secrets.rs:87-112(deliver_secret— raw value delivered, never registered for masking)crates/zeph-subagent/src/agent_loop.rs:684-798(secret injected into tool env; tool output pushed intomessagesunmasked)crates/zeph-subagent/src/agent_loop.rs:186-223(debug dump receives unmaskedmessages)Attack Scenario
A sub-agent is granted a vault secret (e.g.
ZEPH_TELEGRAM_BOT_TOKEN) scoped to a specific tool call. The sub-agent (whether through a legitimate but overly-verbose tool, a buggy skill, or a prompt-injected instruction from untrusted tool output earlier in its own turn) runs a bash command likeenv | grep ZEPHor a curl command with-vthat echoes headers. The raw secret value appears in the tool's stdout, gets wrapped intocontentunmodified, and is: (1) persisted forever in the transcript JSONL, (2) sent to the external LLM provider as part of the next request, and (3) written to disk in--debug-dumpoutput if enabled. Any of these three sinks constitutes a credential leak outside the intended trust boundary.Remediation
Register every value delivered by
deliver_secretintoSecretMaskRegistry(or equivalent) at grant time, and apply that registry's masking pass to tool-resultcontentunconditionally (not just on the hook-replacement branch) before it is pushed intomessages, written to the transcript, or handed to the debug-dump sink — mirroring thezeph-corePAAC placeholder-masking approach used for the top-level agent loop.References
Related: #6460, #6461, #6453 (recent transcript/vault-anchor integrity hardening — this finding is a distinct gap, secret value exposure rather than transcript-tampering)