Vulnerability
Two zeph-sanitizer defense-in-depth controls that specifically exist to prevent sensitive data from reaching third-party LLM providers and on-disk debug dumps ship disabled by default, both in zeph-config struct defaults and in the interactive --init setup wizard:
PiiFilterConfig::default() → enabled: false (crates/zeph-config/src/sanitizer.rs:440-451)
SecretMaskingConfig::default() → enabled: false (crates/zeph-config/src/sanitizer.rs:222-227)
src/init/security.rs:205-210 — PII filter prompt: Confirm::new()...default(false)
src/init/security.rs:380-385 — secret masking prompt: Confirm::new()...default(false)
Both controls are cheap: synchronous, regex-based (PII filter) or in-process placeholder substitution (secret masking) — neither requires an extra LLM call, unlike GuardrailFilter/QuarantinedSummarizer/TurnCausalAnalyzer, whose opt-in-by-default posture is defensible on cost grounds. There is no such cost justification for PII filtering or secret masking, yet an operator who presses Enter through every --init prompt (the common path) ends up with both protections off.
This directly conflicts with the project's own stated security posture. CLAUDE.md states: "ALL secrets and API keys are stored exclusively in the Zeph age vault ... never leak" and secret_mask.rs's own module doc frames PAAC masking as preventing "vault-resolved secrets from appearing in LLM payloads." With the shipped defaults, that guarantee does not hold unless the operator manually opts in.
Severity
Medium (P2) — real but bounded. Exploitation requires tool output (shell, MCP, file read, web scrape) to actually surface a raw secret or PII, which is a plausible but not universal occurrence (e.g. env, reading a config/.env-adjacent file, an MCP server response containing customer records, a script that echoes a resolved API key during debugging). When it happens, the secret/PII is sent verbatim to the external LLM provider and is also captured in cleartext in message-history persistence (SQLite) and in debug dumps (.local/testing/debug/, or the equivalent production debug-dump path) if enabled.
Location
crates/zeph-config/src/sanitizer.rs:439-451 (PiiFilterConfig::default)
crates/zeph-config/src/sanitizer.rs:221-228 (SecretMaskingConfig::default)
src/init/security.rs:205-210, src/init/security.rs:379-386
Attack Scenario
- Operator runs
zeph --init, accepts default answers throughout (a very common setup path — the wizard has 15+ prompts and most operators keep the recommended true defaults, but these two land on false).
- Vault holds
ZEPH_OPENAI_API_KEY etc. per CLAUDE.md's mandatory vault-only policy.
- During a session, a tool call (e.g.
shell running env, or reading a .env-adjacent config file for debugging, or an MCP server that echoes a customer record containing an SSN/email) surfaces the raw secret or PII in its output.
- Because
secret_masking.enabled = false, SecretMaskRegistry is never constructed (src/bootstrap/mod.rs:252-257 gates construction entirely on this flag) — the raw secret is sent unmasked to the external LLM API in the next turn's context, and is written unmasked into SQLite message history and any active debug dump.
- Because
pii_filter.enabled = false, the same applies to any email/phone/SSN/credit-card pattern in the same tool output.
- Neither the LLM response nor the local log/debug-dump path recovers confidentiality after the fact — the data has already left the trust boundary.
Remediation
Pick one:
- Flip both
PiiFilterConfig::default().enabled and SecretMaskingConfig::default().enabled to true, and change the two --init prompts to .default(true), matching the treatment already given to skill_scan_on_load and skill_require_integrity_check_on_promote (both default(true) with a "(recommended)" prompt suffix) elsewhere in the same wizard file.
- If a default flip is judged too risky for existing users on upgrade (behavior change, potential false-positive PII redaction noise), at minimum: (a) mark both prompts
(recommended) like the other security prompts in the same file, and (b) add a config-migration step (per CLAUDE.md's migration-on-add-parameter rule) that emits a one-time startup warning when both remain disabled, so operators on existing configs are made aware of the gap rather than silently inheriting it.
References
Vulnerability
Two zeph-sanitizer defense-in-depth controls that specifically exist to prevent sensitive data from reaching third-party LLM providers and on-disk debug dumps ship disabled by default, both in
zeph-configstruct defaults and in the interactive--initsetup wizard:PiiFilterConfig::default()→enabled: false(crates/zeph-config/src/sanitizer.rs:440-451)SecretMaskingConfig::default()→enabled: false(crates/zeph-config/src/sanitizer.rs:222-227)src/init/security.rs:205-210— PII filter prompt:Confirm::new()...default(false)src/init/security.rs:380-385— secret masking prompt:Confirm::new()...default(false)Both controls are cheap: synchronous, regex-based (PII filter) or in-process placeholder substitution (secret masking) — neither requires an extra LLM call, unlike
GuardrailFilter/QuarantinedSummarizer/TurnCausalAnalyzer, whose opt-in-by-default posture is defensible on cost grounds. There is no such cost justification for PII filtering or secret masking, yet an operator who presses Enter through every--initprompt (the common path) ends up with both protections off.This directly conflicts with the project's own stated security posture. CLAUDE.md states: "ALL secrets and API keys are stored exclusively in the Zeph age vault ... never leak" and
secret_mask.rs's own module doc frames PAAC masking as preventing "vault-resolved secrets from appearing in LLM payloads." With the shipped defaults, that guarantee does not hold unless the operator manually opts in.Severity
Medium (P2) — real but bounded. Exploitation requires tool output (shell, MCP, file read, web scrape) to actually surface a raw secret or PII, which is a plausible but not universal occurrence (e.g.
env, reading a config/.env-adjacent file, an MCP server response containing customer records, a script that echoes a resolved API key during debugging). When it happens, the secret/PII is sent verbatim to the external LLM provider and is also captured in cleartext in message-history persistence (SQLite) and in debug dumps (.local/testing/debug/, or the equivalent production debug-dump path) if enabled.Location
crates/zeph-config/src/sanitizer.rs:439-451(PiiFilterConfig::default)crates/zeph-config/src/sanitizer.rs:221-228(SecretMaskingConfig::default)src/init/security.rs:205-210,src/init/security.rs:379-386Attack Scenario
zeph --init, accepts default answers throughout (a very common setup path — the wizard has 15+ prompts and most operators keep the recommendedtruedefaults, but these two land onfalse).ZEPH_OPENAI_API_KEYetc. per CLAUDE.md's mandatory vault-only policy.shellrunningenv, or reading a.env-adjacent config file for debugging, or an MCP server that echoes a customer record containing an SSN/email) surfaces the raw secret or PII in its output.secret_masking.enabled = false,SecretMaskRegistryis never constructed (src/bootstrap/mod.rs:252-257gates construction entirely on this flag) — the raw secret is sent unmasked to the external LLM API in the next turn's context, and is written unmasked into SQLite message history and any active debug dump.pii_filter.enabled = false, the same applies to any email/phone/SSN/credit-card pattern in the same tool output.Remediation
Pick one:
PiiFilterConfig::default().enabledandSecretMaskingConfig::default().enabledtotrue, and change the two--initprompts to.default(true), matching the treatment already given toskill_scan_on_loadandskill_require_integrity_check_on_promote(bothdefault(true)with a "(recommended)" prompt suffix) elsewhere in the same wizard file.(recommended)like the other security prompts in the same file, and (b) add a config-migration step (per CLAUDE.md's migration-on-add-parameter rule) that emits a one-time startup warning when both remain disabled, so operators on existing configs are made aware of the gap rather than silently inheriting it.References