marshmallow/bot-shield
marshmallow/bot-shield provides lightweight bot protection for Laravel applications. Use configurable middleware and detection rules to identify and block suspicious automated requests, reduce spam, and protect forms and endpoints without complex setup.
Bot Shield sits between Livewire's exception handling and your error reporting. Its job is to keep forged/probe traffic from turning into 500s, Sentry noise, and alert fatigue, while leaving genuine bugs untouched.
Minimal path:
composer require marshmallow/bot-shieldphp artisan bot-shield:install — this publishes config/bot-shield.php.exceptions.rules first. It is the whole package in one array: a list of exception classes, optional contains string matchers, and (as of v1.2.0) optional when conditions.Hardening\Conditions namespace — ForgedMethodName and ForgedReactivePropUpdate are the new v1.2.0 conditions and are the clearest examples of the pattern.First use case: someone (or something) POSTs to /livewire/update with a manipulated snapshot — an array where a scalar belongs, a calls entry with a non-identifier method name, or an updates entry mutating a reactive prop it doesn't own. Today that surfaces as a 500 and an exception report. After install, the request is answered with a client error and nothing is reported — but only if the request looks forged, not merely because the user agent is odd.
Rule shape. Each entry in exceptions.rules is roughly ['class' => ..., 'contains' => [...], 'when' => ConditionClass::class, 'always' => bool]. contains narrows by message fragment; when requires a condition class to also accept the request; always skips the user-agent gate entirely.
Prefer conditions over message matching. Message fragments are brittle across PHP and Livewire versions. The v1.2.0 additions show the better path: MethodNotFoundException is only suppressed when ForgedMethodName confirms the called method isn't a valid identifier — a typo in a real method name still reports. Same for CannotMutateReactivePropException + ForgedReactivePropUpdate: only suppressed when the request's own updates touch that prop on that component. Write your own condition classes implementing ExceptionCondition whenever you catch yourself reaching for a broad contains.
Use contains only as a narrow pair. Note the shape of the new forged-array rules: ['class' => ErrorException::class, 'contains' => ['Attempt to read property', 'on array']] — two fragments that must both be present. Never register Error or TypeError on the class alone; you will swallow real bugs.
Layer the response. Suppression and response are separate concerns in practice. Decide per rule whether you want "don't report" or "don't report and answer a client error." Forged probes deserve a fast 4xx; ambiguous cases are often better silently unreported so the normal handler still shapes the response.
Test the gate, not just the rule. Write feature tests that hit livewire/update with a forged payload (array into a scalar prop, non-identifier method name) and assert no exception was reported; then a test with a genuine typo and assert it was reported. The second half is the one people forget, and it's the one that catches over-broad rules.
Environment control. BOT_SHIELD_FORGED_UPDATES toggles forged-update detection. This is the pattern to copy for anything that alters failure semantics: default on, env-off for the one deploy where you need to bisect.
Publishing replaces, it does not merge. This is the headline upgrade trap. A published exceptions.rules array fully replaces the package defaults, so every new rule in v1.2.0 is inert on an existing install until you paste it in yourself. After any Bot Shield upgrade, diff the package's default config against yours. If you maintain a long-lived published config, consider keeping it thin and only overriding the entries you actually need.
A missing key means "on". exceptions.forged_updates is treated as enabled when absent. So forged-update detection cannot be turned off with BOT_SHIELD_FORGED_UPDATES=false unless the key exists in your config. Add 'forged_updates' => env('BOT_SHIELD_FORGED_UPDATES', true), explicitly — otherwise you'll spend an afternoon wondering why the env var does nothing.
when conditions still run alongside the detector. The condition is an additional gate, not a replacement, unless you also set always => true. The two v1.2.0 Livewire rules use always, which is deliberate: a forged method name is forged regardless of user agent. Don't set always casually on anything message-matched.
Livewire class availability. MethodNotFoundException and CannotMutateReactivePropException live in Livewire internals and can move or disappear between Livewire majors. Registering a rule for a class that doesn't exist is usually harmless but silently useless — verify after upgrading Livewire, and pin your Livewire constraint if you depend on these rules.
Exception class drift across PHP versions. ErrorException vs Error vs TypeError for the "on array" family varies with PHP version and error handler setup. Confirm which class you're actually getting before writing the rule; the contains fragments are the stable part.
Silent suppression hides config bugs. Because Bot Shield removes reporting, a misconfigured rule looks identical to a rule that's working. Log locally at debug level when a rule fires during rollout, or temporarily point the exception at a local sink, so you can see matches before trusting them in production.
Extension point to lean on. Custom ExceptionCondition implementations are the sanctioned way to encode "is this actually forged?" logic. Keep them request-pure and cheap — they run inside exception handling, where a slow condition turns a 500 into a timeout.
How can I help you explore Laravel packages today?