Skip to content

[FEATURE] Pre/post API call hooks in Provider interface for secret redaction and exfiltration prevention #19425

Closed as not planned

Description

@paroque28

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 Provider interface)
  • Requires minimal code changes (wrap at creation time)
  • Is testable (mock the Redactor interface)
  • 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

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Mar 27, 2026
  2. github-actions commented on Mar 27, 2026

    @github-actions
    Contributor

    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.

  3. paroque28 commented on Mar 27, 2026

    @paroque28
    Author

    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 via ANTHROPIC_BASE_URL proxy interception, but native hooks in the Provider interface (as proposed above) would be cleaner and more integrated.

  4. dru89 commented on Apr 6, 2026

    @dru89

    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.before doesn'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.

  5. removed
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  6. github-actions commented on Jul 5, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

  7. BenSharir commented on Aug 16, 2026

    @BenSharir

    Update for anyone finding this: the v2 ctx.session.hook("http.request") / http.response hooks now cover most of what was asked here — a plugin can mutate the outbound Request before dispatch. One gap remains: per the v2 docs these hooks apply to native models only; AI-SDK-routed models don't pass through them (and ctx.aisdk.hook exposes the SDK/model, not the outbound request/body), so providers on a custom OpenAI-compatible baseURL still 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions