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
| Interface | Method | Called when |
|---|---|---|
plugin.ScanStarted | OnScanStarted(ctx, scanID, direction, text) | A scan begins |
plugin.ScanCompleted | OnScanCompleted(ctx, scanID, decision, findingCount, elapsed) | A scan finishes |
plugin.ScanBlocked | OnScanBlocked(ctx, scanID, reason) | Content is blocked |
plugin.ScanFailed | OnScanFailed(ctx, scanID, err) | A scan fails with error |
Safety primitive lifecycle
| Interface | Method | Called when |
|---|---|---|
plugin.InstinctTriggered | OnInstinctTriggered(ctx, scanID, name, score) | Instinct fires |
plugin.AwarenessDetected | OnAwarenessDetected(ctx, scanID, detector, count) | Awareness notices something |
plugin.BoundaryEnforced | OnBoundaryEnforced(ctx, scanID, boundary) | Boundary blocks content |
plugin.ValueViolated | OnValueViolated(ctx, scanID, value, severity) | Value rule violated |
plugin.JudgmentAssessed | OnJudgmentAssessed(ctx, scanID, assessor, risk, confidence) | Judgment completes |
plugin.ReflexFired | OnReflexFired(ctx, scanID, reflex, action) | Reflex triggers action |
Data lifecycle
| Interface | Method | Called when |
|---|---|---|
plugin.PIIDetected | OnPIIDetected(ctx, scanID, piiType, count) | PII found |
plugin.PIIRedacted | OnPIIRedacted(ctx, scanID, piiType, count) | PII redacted |
plugin.PolicyEvaluated | OnPolicyEvaluated(ctx, scanID, policy, decision) | Policy applied |
plugin.SafetyProfileResolved | OnSafetyProfileResolved(ctx, scanID, profile) | Profile loaded |
System
| Interface | Method | Called when |
|---|---|---|
plugin.Shutdown | OnShutdown(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,
),
)