Shield

Custom Plugins

Build plugins that react to Shield lifecycle events with metrics, audit trails, tracing, or custom logic.

Shield uses an opt-in plugin system. Implement the plugin.Plugin base interface and any combination of 15 lifecycle hook interfaces to receive only the events you care about.

The base interface

Every plugin must implement Name:

import "github.com/xraph/shield/plugin"

type MyPlugin struct{}

func (p *MyPlugin) Name() string { return "my-plugin" }

Opting into lifecycle hooks

Implement any subset of the hook interfaces. The registry only dispatches events to plugins that implement the relevant interface.

// Receive scan completion events
func (p *MyPlugin) OnScanCompleted(
    ctx context.Context,
    scanID id.ScanID,
    decision string,
    findingCount int,
    elapsed time.Duration,
) error {
    log.Printf("scan %s completed: %s (%d findings, %s)",
        scanID, decision, findingCount, elapsed)
    return nil
}

// Receive instinct trigger events
func (p *MyPlugin) OnInstinctTriggered(
    ctx context.Context,
    scanID id.ScanID,
    instinctName string,
    score float64,
) error {
    log.Printf("instinct %s triggered (score: %.2f)", instinctName, score)
    return nil
}

Available hooks

Scan lifecycle

InterfaceMethodCalled when
plugin.ScanStartedOnScanStarted(ctx, scanID, direction, text)A scan begins
plugin.ScanCompletedOnScanCompleted(ctx, scanID, decision, findingCount, elapsed)A scan finishes
plugin.ScanBlockedOnScanBlocked(ctx, scanID, reason)Content is blocked
plugin.ScanFailedOnScanFailed(ctx, scanID, err)A scan fails with error

Safety primitive lifecycle

InterfaceMethodCalled when
plugin.InstinctTriggeredOnInstinctTriggered(ctx, scanID, name, score)Instinct fires
plugin.AwarenessDetectedOnAwarenessDetected(ctx, scanID, detector, count)Awareness notices something
plugin.BoundaryEnforcedOnBoundaryEnforced(ctx, scanID, boundary)Boundary blocks content
plugin.ValueViolatedOnValueViolated(ctx, scanID, value, severity)Value rule violated
plugin.JudgmentAssessedOnJudgmentAssessed(ctx, scanID, assessor, risk, confidence)Judgment completes
plugin.ReflexFiredOnReflexFired(ctx, scanID, reflex, action)Reflex triggers action

Data lifecycle

InterfaceMethodCalled when
plugin.PIIDetectedOnPIIDetected(ctx, scanID, piiType, count)PII found
plugin.PIIRedactedOnPIIRedacted(ctx, scanID, piiType, count)PII redacted
plugin.PolicyEvaluatedOnPolicyEvaluated(ctx, scanID, policy, decision)Policy applied
plugin.SafetyProfileResolvedOnSafetyProfileResolved(ctx, scanID, profile)Profile loaded

System

InterfaceMethodCalled when
plugin.ShutdownOnShutdown(ctx)Engine is shutting down

Registering plugins

Register plugins when creating the engine:

eng, err := engine.New(
    engine.WithPlugin(&MyPlugin{}),
    engine.WithPlugin(observability.NewMetricsExtension()),
)

Or via the Forge extension:

ext := extension.New(
    extension.WithPlugin(&MyPlugin{}),
)

Compile-time safety

Use compile-time interface checks to ensure your plugin implements the hooks you intend:

var (
    _ plugin.Plugin         = (*MyPlugin)(nil)
    _ plugin.ScanCompleted  = (*MyPlugin)(nil)
    _ plugin.ScanBlocked    = (*MyPlugin)(nil)
    _ plugin.PIIDetected    = (*MyPlugin)(nil)
)

If you forget to implement a method, the compiler will tell you.

Error handling

Hook errors are logged as warnings but never propagated to the scan. This prevents a broken plugin from disrupting safety evaluation:

// Inside the registry — your plugin errors are logged, never panic
r.logger.Warn("plugin: hook error",
    "hook", "ScanCompleted",
    "plugin", entry.name,
    "error", err,
)

Return errors from your hooks to signal failures. The registry handles them gracefully.

Example: Slack alerting plugin

type SlackAlertPlugin struct {
    webhookURL string
    client     *http.Client
}

func (p *SlackAlertPlugin) Name() string { return "slack-alerts" }

// Only implement the hooks you care about
func (p *SlackAlertPlugin) OnScanBlocked(
    ctx context.Context,
    scanID id.ScanID,
    reason string,
) error {
    msg := fmt.Sprintf("Shield blocked content: %s (scan: %s)", reason, scanID)
    return p.sendSlackMessage(msg)
}

func (p *SlackAlertPlugin) OnInstinctTriggered(
    ctx context.Context,
    scanID id.ScanID,
    instinctName string,
    score float64,
) error {
    if score > 0.9 {
        msg := fmt.Sprintf("High-confidence threat: %s (score: %.2f)", instinctName, score)
        return p.sendSlackMessage(msg)
    }
    return nil
}

Built-in plugins

Shield ships two built-in plugins:

observability.MetricsExtension

Counts all 14 lifecycle events via gu.MetricFactory counters. Implements every hook.

import "github.com/xraph/shield/observability"

metrics := observability.NewMetricsExtension()
// or with a custom factory:
metrics := observability.NewMetricsExtensionWithFactory(fapp.Metrics())

audithook.Extension

Bridges lifecycle events to an audit trail backend via a local Recorder interface. Supports selective action filtering.

import "github.com/xraph/shield/audit_hook"

audit := audit_hook.New(myRecorder,
    audit_hook.WithActions(
        audit_hook.ActionScanBlocked,
        audit_hook.ActionPIIDetected,
    ),
)

On this page