Repository navigation
[FEATURE] Pre/post API call hooks in Provider interface for secret redaction and exfiltration prevention #19425
Description
Problem
When using OpenCode, all code, file contents, command outputs, and user prompts are sent to external LLM API servers (Anthropic, OpenAI, Gemini, etc.). This includes secrets, project names, company names, and NDA-protected material.
Organizations need the ability to prevent sensitive data from leaving the machine — whether to the LLM provider or to attackers via prompt injection — and un-redact AI responses before they reach local tools.
Current plugin limitations
The existing plugin system provides tool.execute.before which intercepts tool calls. But this operates at the tool level, not the API level. There is no way to:
- Redact file contents read by tools before they're sent in the API request
- Redact user prompts before they reach the LLM provider
- Un-redact model responses before tool calls execute
- Cover all providers uniformly (Anthropic, OpenAI, Gemini, Bedrock, etc.)
Proposed Solution
Add two hook points in the Provider interface, firing before/after the actual API call.
Where in the code
The interception point already exists in internal/llm/provider/provider.go:
func (p *baseProvider[C]) SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
messages = p.cleanMessages(messages)
// ← NEW: fire PreApiCall hooks here, allow modifying messages
return p.client.send(ctx, messages, tools)
// ← NEW: fire PostApiCall hooks here, allow modifying response
}
func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
messages = p.cleanMessages(messages)
// ← NEW: fire PreApiCall hooks here, allow modifying messages
return p.client.stream(ctx, messages, tools)
// ← NEW: fire PostApiCall hooks on each event, allow modifying content
}Implementation approach: Go decorator
type RedactingProvider struct {
inner Provider
redactor Redactor // interface with Redact(text) and Unredact(text)
}
func (r *RedactingProvider) SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
redactedMessages := r.redactMessages(messages)
response, err := r.inner.SendMessages(ctx, redactedMessages, tools)
if err != nil {
return nil, err
}
r.unredactResponse(response)
return response, nil
}This:
- Works with all providers (wraps the
Providerinterface) - Requires minimal code changes (wrap at creation time)
- Is testable (mock the
Redactorinterface) - Is opt-in (only activates when configured)
Alternative: Plugin hooks
If a Go-level change is not preferred, expose as plugin events:
api.on("api.request.before", (event) => {
return { messages: redactMessages(event.messages) };
});
api.on("api.response.after", (event) => {
return { response: unredactResponse(event.response) };
});End-to-end flow
1. User: "Check config.py for the AcmeCorp API key"
2. Agent reads file → gets content with real secret
3. Agent builds messages array with tool_result containing the secret
4. ✨ PreApiCall fires → redacts secret → placeholder token
5. Redacted messages sent to LLM provider — secret never leaves machine
6. LLM responds using placeholder token
7. ✨ PostApiCall fires → un-redacts token → real value
8. Agent processes response with real values
9. Tool calls execute with real content
Defense against prompt injection exfiltration
Beyond protecting data sent to the LLM provider, API-level hooks defend against prompt injection attacks that trick the model into exfiltrating secrets via tool calls.
Real-world attack vectors (see gricha/dangerous-skills):
| Attack | Pwn Rate | Vector |
|---|---|---|
| Trojan helper script | 100% | Payload buried in legitimate bash code |
| Memory poisoning | 96% | Persistent backdoor in global config, survives skill removal |
| Hook exploitation | 89% | Skill-defined hooks fire shell commands — model never knows |
| Test file RCE | 60% | conftest.py auto-imported by pytest |
| Symlink exfiltration | 54% | "Example" file is symlink to ~/.ssh/id_rsa |
| Supply chain | 36% | npm postinstall hook |
| Image injection | 30% | Hidden instructions in PNG metadata |
With PreApiCall, a redaction tool scans the entire outgoing request and blocks secrets before they reach any server. With PostApiCall, suspicious tool calls in the response can be stripped before execution.
Why this matters
- Enterprise adoption: Organizations cannot send proprietary code to external APIs without redaction
- GDPR/PII compliance: Employee names, emails, internal data must be scrubbed
- NDA protection: Project names, client names, business logic
- Provider-agnostic: One redaction layer covers all LLM providers
- Zero gaps: Unlike tool-level hooks, API-level hooks cover everything
- Defense-in-depth: Catches exfiltration that tool-level hooks miss
Scope
Minimal change — add 2 hook points in provider.go or expose via plugin API. The actual redaction logic lives in external tools/plugins. OpenCode just needs to provide the interception point.
Activity
github-actions commentedon Mar 27, 2026
This issue might be a duplicate of existing issues. Please check:
Please review those issues and consider whether your proposal can be combined with or builds upon the existing discussions there.
paroque28 commentedon Mar 27, 2026
Reference implementation: We've built claude-code-redact (
rdx) — a working redaction proxy that demonstrates the round-trip redaction flow described above. It currently works viaANTHROPIC_BASE_URLproxy interception, but native hooks in theProviderinterface (as proposed above) would be cleaner and more integrated.dru89 commentedon Apr 6, 2026
This hook would also solve a simpler but related problem: pre-request validation. We're building an AWS Bedrock integration where credentials rotate via SAML federation and expire mid-session. A plugin needs to check credential TTL and attempt refresh before the provider call, or block with a helpful error instead of a raw AWS auth failure.
tool.execute.beforedoesn't cover inference calls, so there's no way to do this today.Our use case doesn't need message mutation — just the ability to run before
streamText()and throw to block. But the same hook point covers both.github-actions commentedon Jul 5, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.
BenSharir commentedon Aug 16, 2026
Update for anyone finding this: the v2
ctx.session.hook("http.request")/http.responsehooks now cover most of what was asked here — a plugin can mutate the outboundRequestbefore dispatch. One gap remains: per the v2 docs these hooks apply to native models only; AI-SDK-routed models don't pass through them (andctx.aisdk.hookexposes the SDK/model, not the outbound request/body), so providers on a custom OpenAI-compatiblebaseURLstill can't have their outbound requests intercepted — which blocks the secret-redaction/egress-audit use case from this issue for those providers. Opened a focused follow-up: #42846.