TL;DR. Your prompts are assembled at runtime from real data: a support ticket, a user row, a document. That data can carry an email address, a payment card, or even one of your own API keys straight to a third party you do not control, with their retention and their subprocessors. The leak is in the data, not the code, so static review and your own eyes cannot see it. This post shows how to scan every outbound AI request in a Laravel app, redact what should not leave, and block the unsafe ones, all at the HTTP layer, regardless of which AI SDK you use. The package is free and open source.
Here is a controller that passes every review:
$summary = $prism->withPrompt(
"Summarise this support ticket for the team:\n\n" . $ticket->body
)->asText();Clean code. The problem is $ticket->body. A customer pasted a card number to "verify their identity," or quoted an email thread, or attached a stack trace with a bearer token in a header. None of that is visible in the source. It only exists at the moment the request goes out, and at that moment it is on its way to OpenAI or Anthropic.
This matters for two separate reasons that often get conflated:
You cannot fix at review time a leak that only exists at runtime. You need something on the wire.
The redaction conversation today is dominated by three approaches, and each has a cost for a Laravel team:
What a Laravel app wants is a guard that lives inside the app, sees every outbound call by default, and does not care which SDK made it.
laravel-ai-egress-guard registers a global HTTP-client middleware. Every outbound request to a known AI provider host is inspected before it leaves: it builds a scan context from the request body, runs the rule set, stores the request and its findings with high-confidence secrets redacted, and, when you turn guard mode on, can stop the request.
composer require laravelsecurityaudit/laravel-ai-egress-guard
php artisan migrateBecause the hook is at Laravel's HTTP-client layer, coverage is broad without per-call wiring. Prism is built on the Laravel HTTP client, so Prism calls are covered automatically, as are the laravel/ai SDK and anything using the Http facade. A client that ships its own Guzzle stack, like openai-php, is the one case you attach by hand:
use LaravelSecurityAudit\EgressGuard\Support\EgressGuard;
$client = new \GuzzleHttp\Client(['handler' => EgressGuard::stack()]);Requests to any non-provider host take a fast path and are ignored, so this is not a tax on the rest of your traffic.
For OpenAI, Anthropic, and Gemini request shapes, the guard extracts the model and a readable prompt (messages[].content, Anthropic system, Gemini contents[].parts[].text) and stores that alongside the raw body. By default it scans the full raw body, which is the most thorough option. If the raw JSON is too noisy for your rules, set scan.target to prompt and it scans only the extracted message text.
The rules combine the shared detection engine with egress-specific patterns:
| Rule id | Severity | Confidence |
|---|---|---|
secrets.private_key | critical | high |
secrets.stripe_key | critical | high |
secrets.api_key | critical | high |
secrets.aws_access_key | critical | high |
secrets.bearer_token | warning | medium |
pii.credit_card | critical | high |
pii.email | warning | medium |
pii.phone | warning | low |
You toggle rules, override severities, and add your own by implementing the engine's Rule contract.
The guard is designed to be adopted gradually, so it never breaks a real AI call on its first day.
Out of the box it captures and redacts but does not block. Open /egress-guard in a non-production environment and you see every captured request with its provider, model, a risk badge, and the findings, with the body redacted. The inbox can hold sensitive prompt data, so it is closed by default unless you set a gate or are in a listed local environment. This is the "show me what we are actually sending" view, and it is usually a surprising first run.
Run your suite so calls are captured, then gate CI:
php artisan test
php artisan egress-guard:scan --min-severity=critical --format=sarif --output=egress-guard.sarifThe command exits non-zero when a finding meets the threshold, and SARIF points at the calling route action when known. You can also assert directly in a test:
use LaravelSecurityAudit\EgressGuard\Support\EgressGuard;
public function test_the_summary_prompt_leaks_nothing(): void
{
$this->generateSummaryFor($user); // makes the AI call
EgressGuard::assertNoCriticalFindings();
}When you trust the thresholds, turn on guard mode:
EGRESS_GUARD_BLOCK=trueDefaults are conservative: it blocks only critical findings at high confidence, and fail_open is true, so a scanner error never silently breaks a real call. A blocked request throws EgressGuardBlocked, fires an EgressBlocked event, and logs the rule ids, never the secret. A known-safe call can be allowlisted by route action or bypassed with a header.
Because the guard already sits on every outbound call and knows the provider, it can enforce where data is allowed to go:
// config/egress-guard.php
'residency' => [
'enabled' => true,
'allowed_regions' => ['EU'],
'regions' => ['openai' => 'EU', 'anthropic' => 'US'],
],A call to a provider whose region is not allowed is blocked exactly like a secret leak and fires a ResidencyViolation event. With enforcement off, the event still fires so you can measure exposure before you start blocking.
The guard does not alter the request you send unless guard mode is on and it blocks. What it redacts is the copy it stores for the inbox and findings, so your own audit trail never becomes a second place the secret lives. A critical, high-confidence match like a card number or a private key is masked in the stored body, and the rest stays readable so a reviewer keeps the context. The redaction is conservative by design: it masks what it is sure about rather than mangling the whole payload.
Only calls to known provider hosts are inspected; everything else takes a fast path. The inspection is an in-process scan of the request body, measured in milliseconds, with no extra network hop.
No. There is no base URL to change and no separate service to run. It is middleware inside your app, on the HTTP client you already use.
The guard inspects the outbound request, the prompt you send, which is where the leak you control lives. It does not need to buffer the streamed response to do that.
Yes, by attaching its handler to that client's Guzzle stack, shown above. SDKs built on Laravel's HTTP client, including Prism, are covered automatically.
laravel-ai-egress-guard is MIT licensed. It pairs with laravel-ai-lint, which catches exposed keys in source before they ship, the circuit breaker that stops runaway spend, and a processing ledger that turns these same captured calls into a GDPR record of processing. Static analysis catches the wiring; the egress guard catches what only appears at runtime.
This is defensive tooling, not legal advice, and not affiliated with or endorsed by Laravel, Laravel LLC, or any AI provider.
A senior engineer reviews every route that touches AI, every prompt builder, every mailable. Fixed price, every finding with a fix.