Repository navigation
feat(ai): add secret shield for outbound credential redaction - #59571
GiacomoSaccaggi wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Requesting changes. The shield runs as described, but it flags a lot of ordinary text as credentials, and that makes block and mask risky to turn on.
I ran the new tests (24 pass) and type-checked packages/ai and packages/core (clean). I called redact() on ordinary text and drove the TUI with the simulated model and OPENCODE_SECRET_SHIELD=block. The prompt "Add Basic authentication to the admin page" was blocked: the session went idle with no reply and no error shown to the user.
Public API (all additions, backwards compatible):
- New config key
secret_shield:"off" | "warn" | "mask" | "block" - New env var
OPENCODE_SECRET_SHIELD - New exports from
@opencode/ai:applySecretShield,redact,scan,redactMessages,redactSystem,redactTools,isSecretShieldMode,REDACTED, plus the typesSecretShieldMode,ShieldReportandShieldResult
Concerns, most important first (details inline):
- False positives.
AUTH_SCHEMEandHEADERmatch prose and source code: "Basic authentication", "OAuth integration",authorization: stringin a type,Authorization: `Bearer ${apiKey}`. An empty"token": ""also counts as a detection. - Block mode is silent and permanent. The user sees no error, and because history is never changed, one flagged string blocks every later step in the session.
- Mask mode can corrupt files. Masked
read/bashoutput leads the model to write[REDACTED]back into the user's files throughedit/write. - Coverage. Only the main runner step is scanned. Title generation, compaction and other direct model calls are not, even though #50904 names title generation as a gap.
- Config key.
secret_shieldis the only snake_case key inConfig.Info; it's new public config surface, so worth settling before it ships.
b3a7371 to
1688b17
Compare
There was a problem hiding this comment.
The new commits fix part of what I raised. The block notice still doesn't reach the user, and two of the false positives remain.
Addressed: the config key moved to experimental.secret_shield. "Basic authentication" and an empty "token": "" are no longer flagged.
Still open:
- Block mode. In the TUI (simulated model,
OPENCODE_SECRET_SHIELD=block), a prompt containing aghp_…key ended the turn with nothing shown. A follow-up "Never mind, just say hello" was silently blocked too. Details inline. - False positives.
authorization: string;and "Bearer re-authentication" are still redacted. Details inline. - Known limitations. Mask mode can still write
[REDACTED]into files, and only the main runner step is scanned. Both are now documented in the description, not fixed.
Tests (27) pass; packages/core type-checks.
Public API (all additions, backwards compatible):
- New config key
experimental.secret_shield:"off" | "warn" | "mask" | "block" - New env var
OPENCODE_SECRET_SHIELD - New exports from
@opencode/ai:applySecretShield,redact,scan,redactMessages,redactSystem,redactTools,isSecretShieldMode,REDACTED, plus the typesSecretShieldMode,ShieldReportandShieldResult
1688b17 to
112fa23
Compare
There was a problem hiding this comment.
The new commits fix the false positives and the setting name in the block message. Block mode still fails silently, and the rebase deleted three existing comments.
Addressed:
authorization: string;, "Bearer re-authentication" andAuthorization: `Bearer ${apiKey}`are no longer redacted. Real values likeAuthorization: Basic dXNl…==still are.- The block message now names
experimental.secret_shieldandOPENCODE_SECRET_SHIELD.
Still open:
- Block mode. Block is back to raising
StepFailedError, as in the first version. In the TUI (simulated model,OPENCODE_SECRET_SHIELD=block), a prompt containing aghp_…key ended with nothing on screen, and a follow-up "Never mind, just say hello" was silently blocked too. The user needs a visible error and a way to recover the session. - Mask mode and coverage are documented as known limitations, not fixed.
New: the rebase deletes three comments that dev keeps in llm.ts (details inline).
Tests (27) pass; packages/core type-checks.
Public API (all additions, backwards compatible):
-
New config key
experimental.secret_shield:"off" | "warn" | "mask" | "block" -
New env var
OPENCODE_SECRET_SHIELD -
New exports from
@opencode/ai/effect:applySecretShield,isSecretShieldMode, plus the typesSecretShieldMode,ShieldResultandShieldReport -
packages/core/src/session/runner/llm.ts:251: The rebase deleted three comments thatdevkeeps here, and they aren't related to this feature: - the note ontoolChoiceabout keeping tool definitions on the final step to preserve the provider's cached prefix - the note on thepreparedmapping explaining why only these fields are kept (memory held by the hook event while slow tools run) - the note onexecuteToolabout tool calls arriving during a plugin reload Please restore them so the diff only contains the shield changes.
Scan LLM requests for credential patterns before sending to providers. Four modes: off (default), warn, mask, block. Detects 16 API key prefix families, JSON sensitive fields, HTTP auth headers, connection string URIs, and JWT tokens. Scans text, tool-result, reasoning, and tool-call parts. Auth scheme and header patterns require a digit in the value to avoid matching prose and type declarations. JSON sensitive fields skip empty values. Structured values walked recursively preserving object shapes. Config under experimental.secret_shield while rules settle.
27 tests covering pattern families, false-positive boundaries (prose, type declarations, template literals, empty values), tool-result and reasoning scanning, structure preservation, and all four modes. Runner: mode from experimental config with env var fallback; block publishes Step.Started + Step.Failed for TUI visibility with recovery instructions.
112fa23 to
8794914
Compare
There was a problem hiding this comment.
Both points from my last review are fixed. A blocked request now shows a visible error on the turn, with instructions to continue, and the three deleted comments in llm.ts are restored. In the TUI (simulated model, OPENCODE_SECRET_SHIELD=block), a prompt containing a ghp_… key showed the Secret Shield error. A follow-up message showed the same error, since the key is still in the history. Tests (27) pass; packages/core type-checks.
Nothing new to flag. Mask mode and coverage remain the known limitations listed in the description.
Public API (all additions, backwards compatible):
- New config key
experimental.secret_shield:"off" | "warn" | "mask" | "block" - New env var
OPENCODE_SECRET_SHIELD - New exports from
@opencode/ai/effect:applySecretShield,isSecretShieldMode, plus the typesSecretShieldMode,ShieldResultandShieldReport
|
All review feedback has been addressed across 6 iterations:
Known limitations documented in the description: mask mode can write Tests (27) pass, typecheck clean on ai/schema/core. |
There was a problem hiding this comment.
The only new commit merges dev into the branch. The PR's own diff is unchanged since my last review, and none of the merged commits touch the files this PR changes. Tests (27) still pass and packages/core still type-checks. Nothing new to flag.
Public API (all additions, backwards compatible):
- New config key
experimental.secret_shield:"off" | "warn" | "mask" | "block" - New env var
OPENCODE_SECRET_SHIELD - New exports from
@opencode/ai/effect:applySecretShield,isSecretShieldMode, plus the typesSecretShieldMode,ShieldResultandShieldReport

Issue for this PR
Closes #50904
Type of change
What does this PR do?
Adds an opt-in credential scanning/redaction layer that intercepts outbound LLM requests before they reach providers. The shield scans system prompts, user messages, tool-result outputs (bash/read), tool-call inputs, reasoning parts, and tool descriptions for credential patterns.
Detection: 16 API key prefix families (OpenAI, Anthropic, GitHub, AWS, Stripe, Slack, etc.), JSON sensitive field names (password, token, api_key, etc.), HTTP auth headers, connection string URIs, and JWT tokens. Prefix patterns are left-anchored to avoid false positives inside identifiers. Auth scheme and header patterns require token-like values (mixed character classes) so prose like "Basic authentication" and type declarations like
authorization: stringare not flagged. Empty JSON values ("token": "") are skipped.Structured value handling: Tool-call inputs and JSON tool results are walked recursively — only string leaves are redacted, preserving object shapes so providers receive valid structured data. Values under sensitive keys (password, token, etc.) are masked unconditionally.
Four modes (via
experimental.secret_shieldconfig key orOPENCODE_SECRET_SHIELDenv var):off(default) — no scanningwarn— scan and log detections, send unmodifiedmask— replace detected credentials with[REDACTED]block— reject the request with a visible synthetic message explaining how to recoverConfig lives under
experimentalwhile detection rules settle. Mode resolution lives in core (config first, env var fallback). The ai package takes an explicitSecretShieldModeargument with no side effects.Known limitations:
[REDACTED]) may be written back into files by the model. Users should be aware of this trade-off.Files changed:
packages/ai/src/secret-shield.ts— redaction engine with recursive structured-value walkerpackages/ai/src/index.ts— public exportspackages/schema/src/config/experimental.ts—secret_shieldconfig fieldpackages/core/src/session/runner/llm.ts— shield wired after request preparation, beforesteps.attempt(); block publishes visible synthetic messagepackages/ai/test/secret-shield.test.ts— 27 testsPort of #50905 to the V2 architecture. Previous PRs: #58410 (auto-closed for missing template).
How did you verify your code works?
bun typecheckclean on packages/ai, packages/schema, packages/core.redact("Use Basic authentication for the admin panel")reports 0 detections,redact("OAuth integration")reports 0,redact("authorization: string")reports 0,redact('"token": ""')reports 0.Screenshots / recordings
N/A — no UI changes.
Checklist
Added by the opencode bot (AI-generated).
Public API: adds the
experimental.secret_shieldconfig key, theOPENCODE_SECRET_SHIELDenv var and secret-shield exports from@opencode/ai/effect(backwards compatible)