PHPackages                             marshmallow/bot-shield - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. marshmallow/bot-shield

ActiveLibrary

marshmallow/bot-shield
======================

Bot traffic hardening and spam protection for Laravel + Livewire: exception noise suppression, honeypot, reCAPTCHA, crawler rules and monitoring.

v1.0.0(today)00MITPHPPHP ^8.3CI failing

Since Jul 30Pushed todayCompare

[ Source](https://github.com/marshmallow-packages/bot-shield)[ Packagist](https://packagist.org/packages/marshmallow/bot-shield)[ Docs](https://github.com/marshmallow-packages/bot-shield)[ RSS](/packages/marshmallow-bot-shield/feed)WikiDiscussions main Synced today

READMEChangelog (2)Dependencies (12)Versions (2)Used By (0)

[![marshmallow.](https://camo.githubusercontent.com/f5450f299f5713ce2f04dd5a1ba7ce9960ed4568b3574e4c4ee3cddc75477253/68747470733a2f2f6d617273686d616c6c6f772e6465762f63646e2f6d656469612f6c6f676f2d7265642d3233377834362e706e67 "marshmallow.")](https://camo.githubusercontent.com/f5450f299f5713ce2f04dd5a1ba7ce9960ed4568b3574e4c4ee3cddc75477253/68747470733a2f2f6d617273686d616c6c6f772e6465762f63646e2f6d656469612f6c6f676f2d7265642d3233377834362e706e67)

Bot Shield
==========

[](#bot-shield)

[![Latest Version on Packagist](https://camo.githubusercontent.com/851dbd6b779ce55aa6b136df1b347888c63dfabd5b3267d0322057c6ece1ee04/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d617273686d616c6c6f772f626f742d736869656c642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/marshmallow/bot-shield)[![Tests](https://camo.githubusercontent.com/d1d2150aa529079f945c9654711bddf48ead2e8958ba8f77c4ce8ecdb06c2945/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d617273686d616c6c6f772d7061636b616765732f626f742d736869656c642f74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/marshmallow-packages/bot-shield/actions/workflows/tests.yml)[![PHP from Packagist](https://camo.githubusercontent.com/0b14ca12fc53de31a51f8a2560a165266636896e17a97f2d50b043d8a7288b36/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f6d617273686d616c6c6f772f626f742d736869656c642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/marshmallow/bot-shield)[![Total Downloads](https://camo.githubusercontent.com/3a3e5f42e5933f4e09f05fbeb28950abd60a3f0cac366c2c406689a482d2c850/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d617273686d616c6c6f772f626f742d736869656c642e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/marshmallow/bot-shield)

Bot traffic hardening and spam protection for Laravel + Livewire: exception noise suppression, honeypot, reCAPTCHA, crawler rules and monitoring.

Bots probing Livewire endpoints throw exceptions that say nothing about your application, and they fill your forms with spam. Bot Shield handles both from one config file: it stops the noise reaching Sentry, protects forms with a honeypot and reCAPTCHA, and records what it blocked so you can tune the thresholds against real traffic instead of guesswork.

Every feature is independently toggleable, and features that need keys disable themselves when the keys are missing. A site that only wants the exception hardening does not have to install anything else.

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 12 or 13
- Livewire 3 or 4, for the Livewire attributes only

Installation
------------

[](#installation)

Install the package via Composer:

```
composer require marshmallow/bot-shield
```

Run the installer. It publishes the config, wires the exception handler into `bootstrap/app.php`, and adds the env keys to `.env.example`:

```
php artisan bot-shield:install
```

The edit to `bootstrap/app.php` is purely additive: it inserts one `BotShield::handles($exceptions)` call directly after the opening brace of your `withExceptions()` closure and leaves everything already in there untouched, whatever order it is in. If it cannot find the closure it changes nothing and prints the line for you to paste.

If you use the monitoring table, publish and run the migration:

```
php artisan vendor:publish --tag="bot-shield-migrations"
php artisan migrate
```

Then confirm the setup:

```
php artisan bot-shield:doctor
```

### Optional dependencies

[](#optional-dependencies)

These are suggested rather than required, so an exception-only install stays lean. The package tells you which one is missing if you enable a feature without it.

```
composer require spatie/laravel-honeypot    # the honeypot form fields
composer require jaybizzle/crawler-detect   # the stricter bot detector
```

### Publishing other resources

[](#publishing-other-resources)

```
php artisan vendor:publish --tag="bot-shield-config"
php artisan vendor:publish --tag="bot-shield-views"
php artisan vendor:publish --tag="bot-shield-lang"
php artisan vendor:publish --tag="bot-shield"        # everything at once
```

Getting started
---------------

[](#getting-started)

A walkthrough from an empty install to one protected form. Each step says how to tell it worked.

**1. Install and wire.**

```
composer require marshmallow/bot-shield
php artisan bot-shield:install
```

Check: `php artisan bot-shield:doctor` reports `Exception hardening ... wired`. If it says `not wired`, the installer could not edit `bootstrap/app.php`; add the `withExceptions()` call from the next section by hand.

**2. Confirm the exception noise is handled.**

Nothing else is needed for this part. Bot-induced Livewire exceptions stop reaching your error tracker from now on, and matched exceptions answer a client error instead of a 500. If that is all you wanted, stop here and turn off the rest:

```
BOT_SHIELD_HONEYPOT_ENABLED=false
BOT_SHIELD_RECAPTCHA_ENABLED=false
BOT_SHIELD_MONITORING_ENABLED=false
BOT_SHIELD_RATE_LIMIT_ENABLED=false
BOT_SHIELD_PROBE_PATHS_ENABLED=false
```

**3. Turn on monitoring, so the later steps are measurable.**

```
php artisan vendor:publish --tag="bot-shield-migrations"
php artisan migrate
```

Then schedule pruning, or the table grows forever:

```
Schedule::command('model:prune', [
    '--model' => [Marshmallow\BotShield\Models\BotShieldEvent::class],
])->daily();
```

Check: `bot-shield:doctor` reports `Monitoring ... recording`.

**4. Add the honeypot to one form.**

```
composer require spatie/laravel-honeypot
```

In the form's view:

```

```

For a Livewire component, add `livewire-model="honeypotData"` to that tag, give the component a `public HoneypotData $honeypotData;`, initialise it in `mount()`, use the `ProtectsAgainstSpam` trait, and call `$this->protectAgainstSpam()` at the top of the submit action. Both variants are shown under [Protecting forms](#protecting-forms).

Check: view the page source and you should see a hidden `div` with two inputs. If nothing renders, the honeypot is disabled or spatie is missing, and `bot-shield:doctor` says which.

**5. Add reCAPTCHA to the same form.**

Create a key in the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin/create). Pick **score based (v3)** for the default driver, or **challenge (v2)** and set `BOT_SHIELD_CAPTCHA_DRIVER=google-v2`. Add every domain the form is served from, and see [Local development](#local-development) before you paste production keys into your own `.env`. The console gives you a site key and a secret key:

```
BOT_SHIELD_RECAPTCHA_SITE_KEY=your-site-key
BOT_SHIELD_RECAPTCHA_SECRET_KEY=your-secret-key
```

In the view:

```

```

Then verify on submit. For Livewire, add `#[ValidatesRecaptcha]` to the action and `property="gRecaptchaResponse"` to the tag. For a classic form, add the rule to your validation, which the `ValidatesRecaptcha` trait does for you in a FormRequest.

Check: `bot-shield:doctor` reports `Captcha ... google-v3, threshold 0.6`. Submit the form once, then `php artisan bot-shield:stats` shows one captcha verification. If the component renders nothing, the keys are missing.

Set `BOT_SHIELD_RECAPTCHA_HIDE_BADGE=true` if you do not want Google's badge in the corner, see [Hiding the badge](#hiding-the-badge).

> Deployed sites keep their env in the Forge UI, so set both keys there as well as locally.

**6. Guard the action against known bots.**

Add `#[BlocksBots]` to a Livewire submit action, or call `protectAgainstBots()` from the `ProtectsAgainstBots` trait. This refuses agents matched by a `block` or `deny` rule, which by default covers search engines, AI crawlers and scripted clients.

Check: `bot-shield:stats` shows a blocked submission after a `curl` POST to the form.

**7. Optional, stop the scanner traffic.**

Register the probe firewall first in the global middleware stack, as shown under [Middleware](#middleware). Check: `bot-shield:stats` starts listing probe path hits within a day on any public site.

**8. After a week, tune the threshold.**

```
php artisan bot-shield:scores --days=7
```

It prints the score distribution from your own traffic and suggests a threshold, or says there is not enough data yet. Change `BOT_SHIELD_RECAPTCHA_SCORE` only if the histogram agrees.

Writing tests for the form you just protected? See [Testing your own application](#testing-your-own-application), which lets you script captcha answers instead of calling Google.

Exception hardening
-------------------

[](#exception-hardening)

`bot-shield:install` wires this for you. To do it by hand, call `BotShield::handles()` inside `withExceptions()`:

```
use Illuminate\Foundation\Configuration\Exceptions;
use Marshmallow\BotShield\Facades\BotShield;

->withExceptions(function (Exceptions $exceptions) {
    BotShield::handles($exceptions);
})
```

If your application still binds its own `App\Exceptions\Handler`, use the trait instead:

```
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Marshmallow\BotShield\Concerns\HardensAgainstBots;

class Handler extends ExceptionHandler
{
    use HardensAgainstBots;

    public function register(): void
    {
        $this->hardenAgainstBots();
    }
}
```

Either way, the malformed-Livewire exception set is no longer reported when the request came from a bot, and real browsers still report normally. Matched exceptions render as a client error rather than a 500, because malformed component state is never a server fault.

> Two Livewire exceptions, `CorruptComponentPayloadException` and `CannotUpdateLockedPropertyException`, define their own `render()` method and answer 419 in production. Laravel consults an exception's own `render()` before any package callback, so those keep their 419. That is still a client error rather than a 500, which is the point.

Add your own patterns without waiting for a release:

```
// config/bot-shield.php
'exceptions' => [
    'rules' => [
        ['class' => YourVendor\SomeException::class],
        ['class' => TypeError::class, 'contains' => ['must be of type']],
    ],
],
```

Each rule matches when the exception is an instance of `class` and its message contains every needle in `contains`.

Agent rules
-----------

[](#agent-rules)

One ordered list of user agent rules, evaluated before any detector, so the first match always wins. This is what catches a bot spoofing a browser user agent, and what stops a monitoring service that looks like a script from being treated as one.

ActionReportedPage viewsForm submissionsrobots.txt`allow`yesallowedallowednot listed`ignore`noallowedallowednot listed`challenge`noallowedcaptcha forcednot listed`block`noallowedrefusednot listed`deny`norefused\*refused`Disallow: /`\* Only once `crawlers.deny_page_views` is on and the `DenyAgents` middleware is registered.

Search engines ship as `block` rather than `deny` on purpose: they must keep crawling your pages, they just have no business submitting a form. AI crawlers ship as `deny`.

```
'agents' => [
    'rules' => [
        ['pattern' => 'Oh Dear', 'action' => 'allow'],
        ['pattern' => 'Googlebot', 'action' => 'block'],
        ['pattern' => 'GPTBot', 'action' => 'deny'],
        ['pattern' => '/^curl\/[0-9]+/', 'action' => 'block', 'regex' => true],
    ],
],
```

Patterns are case-insensitive substrings unless `regex` is true. A malformed rule is discarded rather than allowed to break request handling, and `bot-shield:doctor` reports any it had to drop.

### Detector drivers

[](#detector-drivers)

`user-agent` (the default) treats any user agent without `Mozilla/` as a bot. `crawler-detect` matches against crawler-detect's maintained list instead. You can also name your own class implementing `Marshmallow\BotShield\Contracts\BotDetector`.

```
'detector' => env('BOT_SHIELD_DETECTOR', 'user-agent'),
```

Protecting forms
----------------

[](#protecting-forms)

### Livewire

[](#livewire)

```
use Livewire\Component;
use Marshmallow\BotShield\Livewire\BlocksBots;
use Marshmallow\BotShield\Livewire\RateLimitsSubmissions;
use Marshmallow\BotShield\Livewire\ValidatesRecaptcha;

class ContactForm extends Component
{
    public string $gRecaptchaResponse = '';

    #[BlocksBots]
    #[ValidatesRecaptcha]
    #[RateLimitsSubmissions(attempts: 3, seconds: 60)]
    public function submit(): void
    {
        // Only runs once every check passes.
    }
}
```

> Two different classes are called `ValidatesRecaptcha`. The Livewire **attribute** is `Marshmallow\BotShield\Livewire\ValidatesRecaptcha`, used as `#[ValidatesRecaptcha]` above. The FormRequest **trait** is `Marshmallow\BotShield\Concerns\ValidatesRecaptcha`, shown under [Classic forms](#classic-forms). Import the wrong one and the attribute fails in a way that does not mention the import, so check the namespace first if it misbehaves.

In the component's view:

```

    Send

```

For components that cannot use attributes, the equivalent traits are available:

```
use Marshmallow\BotShield\Concerns\ProtectsAgainstBots;
use Marshmallow\BotShield\Concerns\ProtectsAgainstSpam;
use Spatie\Honeypot\Http\Livewire\Concerns\HoneypotData;

class ContactForm extends Component
{
    use ProtectsAgainstBots;
    use ProtectsAgainstSpam;

    public HoneypotData $honeypotData;

    public function mount(): void
    {
        $this->honeypotData = new HoneypotData;
    }

    public function submit(): void
    {
        $this->protectAgainstBots();
        $this->protectAgainstSpam();
    }
}
```

> Use this package's `ProtectsAgainstSpam` rather than spatie's own `UsesSpamProtection` if you want honeypot trips recorded. Both stop the submission, but only this one records the event, so keeping spatie's trait means `bot-shield:stats` reports `Honeypot trips: 0` forever and `assertHoneypotTripped()` can never pass. Keeping the wiring you already have is otherwise a perfectly reasonable minimal adoption.

### Classic forms

[](#classic-forms)

```

    @csrf

    Send

```

Validate with the rule, or let the trait add it for you:

```
use Illuminate\Foundation\Http\FormRequest;
use Marshmallow\BotShield\Concerns\ValidatesRecaptcha;

class ContactRequest extends FormRequest
{
    use ValidatesRecaptcha;

    public function rules(): array
    {
        return array_merge($this->recaptchaRules('contact'), [
            'email' => ['required', 'email'],
        ]);
    }
}
```

The trait adds nothing while no captcha is active, so a site without keys is never asked for a token it never rendered. Using the rule directly works too:

```
use Marshmallow\BotShield\Rules\Recaptcha;

'g-recaptcha-response' => [new Recaptcha],
```

The rule is implicit, so it runs even when the field is absent. A scripted submission that simply omits the token is refused rather than skipped.

reCAPTCHA
---------

[](#recaptcha)

Keys come from the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin/create): choose score based (v3) for the default driver, or challenge (v2) with `BOT_SHIELD_CAPTCHA_DRIVER=google-v2`. The domain list on the key has to include every host that serves the form.

Set the keys and you are done. Leave them empty and the captcha disables itself: nothing is rendered and nothing is verified.

```
BOT_SHIELD_RECAPTCHA_SITE_KEY=
BOT_SHIELD_RECAPTCHA_SECRET_KEY=
BOT_SHIELD_CAPTCHA_DRIVER=google-v3
BOT_SHIELD_RECAPTCHA_SCORE=0.6
```

Drivers are `google-v3` (score based, invisible), `google-v2` (checkbox or invisible challenge) and `null`. The component and the `@botShieldRecaptcha` directive render whichever applies:

```

@botShieldRecaptcha
@botShieldRecaptcha(['size' => 'invisible'])
```

### Moving the badge

[](#moving-the-badge)

The invisible v2 widget takes a position, which is the least invasive way to get the badge out of your layout:

```
BOT_SHIELD_RECAPTCHA_BADGE=inline
```

`bottomright` is the default, `bottomleft` the mirror of it, and `inline` drops the badge into the flow of the form where you placed the component so you can position it yourself. Per form:

```

```

This reaches the invisible widget only. The v3 widget gives no control over the badge position, and the v2 checkbox has no badge at all, so hiding is the only option there.

### Hiding the badge

[](#hiding-the-badge)

The v3 and invisible widgets park a badge in the corner of the page. Google allows you to hide it only when the reCAPTCHA notice is visible instead, so turning one on turns the other on:

```
BOT_SHIELD_RECAPTCHA_HIDE_BADGE=true
```

Or per form, either way round:

```

@botShieldRecaptcha(['hideBadge' => true])
```

The rule is `visibility: hidden !important`, not `display: none`. Taking the badge out of the layout stops the widget from working, and the `!important` is required rather than defensive: Google sets the badge's visibility in an inline style attribute, which no ordinary stylesheet rule can override. If you already hide the badge in your own CSS, that is the same rule you need, and you can leave this off and use `show_terms` for the notice alone.

Hiding wins over the position: set both and the badge stays hidden. Publish the views with `--tag=bot-shield-views` to reword the notice.

Every verification is logged and recorded with its score, pass or fail. That is what makes the threshold reviewable, see `bot-shield:scores` below.

When Google cannot be reached the submission is refused, because an outage should not become an open door. Set `fail_open` to `true` if losing genuine leads is the worse cost for your site.

### Local development

[](#local-development)

Simplest: leave both keys out of your local `.env`. The captcha needs a site key and a secret key before it does anything, so without them the component renders nothing, `verify()` returns skipped, and your forms submit exactly as they would with the captcha switched off. No local override needed.

Do not copy production keys into a local `.env`. A key only works on the domains listed against it, so on an unlisted host Google refuses to solve the challenge client side, the token field stays empty, and the submission is refused as a missing token. The form looks broken and nothing in the logs blames the domain. `bot-shield:doctor` cannot catch this either: it sees two keys and reports the captcha as active.

If you do want to exercise the real thing locally, add `localhost` and your local domain, `example.test` and the like, to the key's domain list in the console. A separate key for local work keeps that list off the production key.

To switch it off explicitly instead, per environment:

```
BOT_SHIELD_RECAPTCHA_ENABLED=false
```

Or leave the captcha enabled and drop it to the null driver with `BOT_SHIELD_CAPTCHA_DRIVER=null`. Every other part of the package keeps working either way.

For automated tests, do not disable it. `BotShield::fake()` scripts the answers without calling Google, see [Testing your own application](#testing-your-own-application).

### When the token is fetched

[](#when-the-token-is-fetched)

Neither widget waits for the submit button, because asking at submit time means waiting for Google's network round trip before the form can post.

- **v3** asks on page load and refreshes every 100 seconds, since a token expires after two minutes and a form can be open far longer than that.
- **Invisible v2** asks on page load and once more on the form's first interaction. It has no refresh loop. The second ask exists so a form that took a while to fill in submits a fresh token rather than an expired one.

Two consequences worth knowing if you are replacing a submit-time implementation:

- At least one `grecaptcha.execute` runs per visitor to a page carrying a form, not per visitor who actually submits, and an interacting visitor on invisible v2 costs two. Busy sites should keep an eye on their reCAPTCHA quota.
- The v3 score describes behaviour up to page load rather than the submit interaction. Scores are not directly comparable with a submit-time setup, so re-read `bot-shield:scores` before reusing an old threshold.

### Migrating from a submit-time token

[](#migrating-from-a-submit-time-token)

If your form fetches the token in a submit handler and hands it to Livewire itself, it probably looks like this:

```

```

```
#[On('formSubmitted')]
public function submit(?string $token = null): void
```

Adopting the package's component means:

1. Replace the handler with a plain ``. The wrapper element that existed only to own the `@submit.prevent` goes, but **what was inside it stays**: the honeypot component usually lives in there, and losing it is silent, since a form with no honeypot still submits perfectly well. Move its contents into the new `` before deleting the tag.
2. Add `` and a matching `public string $gRecaptchaResponse = '';`. The component fetches and refreshes the token itself, so no JavaScript of yours remains.
3. Delete the `#[On(...)]` listener and the `?string $token` parameter, and put `#[ValidatesRecaptcha]` on the action instead.
4. Move any test or client-side reference from your own error key to `g-recaptcha-response`, which is the field the rule and the attribute both report against.

Deleting the event listener is worth more than it looks. `dispatchSelf` fires every listener for that event name in the component, so a second listener added later would silently run on submit; after the migration there is no dispatch left to go wrong.

**Before you ship, confirm the package's translations resolve:**

```
php artisan tinker --execute="echo trans('bot-shield::messages.recaptcha.failed');"
```

If that prints the key back rather than a sentence, your application cannot read the package's lang files, and a visitor who fails the captcha will read `bot-shield::messages.recaptcha.failed` on the form. Nothing throws and nothing is logged, and a test asserting the error field rather than the message text still passes.

This bites hardest on exactly the sites this migration suits. Hand written captcha code usually passes the sentence itself as the translation key, so it renders correctly even when nothing resolves; moving to `bot-shield::messages` is what turns a long standing loader problem into visible garbage. `bot-shield:doctor` checks this for you.

Middleware
----------

[](#middleware)

Neither is registered for you. The probe firewall belongs first in the global stack; refusing crawler page views is a decision per site.

```
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->prepend(\Marshmallow\BotShield\Http\Middleware\BlockProbePaths::class);
    // $middleware->append(\Marshmallow\BotShield\Http\Middleware\DenyAgents::class);
})
```

Aliases `bot-shield.probe-paths` and `bot-shield.deny-agents` are also registered for use on route groups.

`BlockProbePaths` answers known scanner paths (`wp-login.php`, `.env`, `xmlrpc.php` and friends) with an immediate response. It returns rather than aborts, so there is no exception to report and no session to start.

robots.txt
----------

[](#robotstxt)

Turn every `deny` rule into robots.txt stanzas:

```
php artisan bot-shield:robots
php artisan bot-shield:robots --append=public/robots.txt
```

Search engines are never emitted, because they are `block` rather than `deny`.

Monitoring
----------

[](#monitoring)

One row per notable event in `bot_shield_events`: captcha verifications with their score, honeypot trips, blocked submissions, suppressed exceptions and probe path hits.

```
php artisan bot-shield:stats --days=7
php artisan bot-shield:scores --days=30 --form=contact
```

`bot-shield:scores` prints the score distribution and suggests a threshold from the widest quiet stretch between the clusters in your traffic. It refuses to suggest anything below 50 samples, and says so when the scores do not separate, rather than inventing a number.

Recording is best effort and swallows its own failures. It runs inside form submissions and inside the exception handler, so a missing table never becomes an outage; the structured log channel stays the durable record either way.

Rows are pruned with Laravel's `model:prune`, so schedule it:

```
Schedule::command('model:prune', [
    '--model' => [Marshmallow\BotShield\Models\BotShieldEvent::class],
])->daily();
```

Testing your own application
----------------------------

[](#testing-your-own-application)

```
use Marshmallow\BotShield\Facades\BotShield;

$shield = BotShield::fake();

$shield->captchaPasses(0.9);   // or captchaFails(), captchaScores(0.2), captchaUnavailable()

// ... exercise your form ...

$shield->assertChallenged();
$shield->assertCaptchaPassed();
$shield->assertBlocked('contact');
$shield->assertNotBlocked();
$shield->assertHoneypotTripped();
$shield->assertRateLimited();
```

Captcha answers are scripted, so nothing reaches Google, and events are kept in memory rather than the database. A blank token still fails under the fake, because that bypass is real behaviour rather than a stub detail.

The fake answers as whichever driver your application configures, so the widget your tests render is the widget you ship. Pass a driver name to override that when a test is specifically about another one:

```
BotShield::fake('google-v2');
```

To take the package out of the way entirely for tests that are about something else:

```
BotShield::fake()->withoutProtection();
```

Extending
---------

[](#extending)

Ask whether the current request looks like a bot anywhere in your own code. Agent rules are applied before the detector, so this answers with the same verdict the rest of the package uses:

```
use Marshmallow\BotShield\Facades\BotShield;

if (BotShield::isBot()) {
    // Skip the expensive thing, or the analytics hit.
}

BotShield::isBot($request);      // or pass a specific request
BotShield::detector();           // the resolved detector, rules included
```

Both moving parts are swappable by naming your class in config. A detector implements `Marshmallow\BotShield\Contracts\BotDetector`:

```
use Illuminate\Http\Request;
use Marshmallow\BotShield\Contracts\BotDetector;

class IpRangeDetector implements BotDetector
{
    public function isBot(Request $request): bool
    {
        return YourIpRanges::contains($request->ip());
    }
}
```

```
'detector' => \App\BotShield\IpRangeDetector::class,
```

A captcha driver implements `Marshmallow\BotShield\Contracts\CaptchaDriver` and returns a `CaptchaVerdict`, which is how a provider other than reCAPTCHA (Cloudflare Turnstile, for instance) can be added without touching the rest of the package:

```
'captcha' => [
    'driver' => \App\BotShield\TurnstileDriver::class,
],
```

Your class is resolved from the container, so it can take constructor dependencies. Both are validated at resolve time and report a clear error through `bot-shield:doctor` if they do not implement the contract.

Features and defaults
---------------------

[](#features-and-defaults)

Nothing here is mandatory. Every feature can be turned off, and `BOT_SHIELD_ENABLED=false` turns off all of them at once. "Default" is what a fresh install does with no config changes.

Two features read as on but do nothing until you act: exception hardening needs `BotShield::handles()` called, and the probe firewall needs its middleware registered. `bot-shield:doctor` catches the first.

FeatureDefaultAlso needsToggleLivewire exception suppressionon`BotShield::handles()` or the trait`BOT_SHIELD_EXCEPTIONS_ENABLED`Transient database error suppression**off**`BOT_SHIELD_TRANSIENT_ERRORS_ENABLED`Custom 4xx suppression**off**`exceptions.client_errors.classes``BOT_SHIELD_CLIENT_ERRORS_ENABLED`Agent allow and block ruleson, ~40 rules`agents.rules`, config onlyForm guardon`#[BlocksBots]` on an action`BOT_SHIELD_FORM_GUARD_ENABLED`Refuse everything the detector flags**off**`BOT_SHIELD_FORM_GUARD_USE_DETECTOR`Submission rate limitingon`#[RateLimitsSubmissions]` on an action`BOT_SHIELD_RATE_LIMIT_ENABLED`reCAPTCHAon, inert without keysthe keys, and the blade component`BOT_SHIELD_RECAPTCHA_ENABLED`Accept submissions during a captcha outage**off**`BOT_SHIELD_RECAPTCHA_FAIL_OPEN`Report a captcha outage to Sentry**off**`BOT_SHIELD_RECAPTCHA_REPORT_OUTAGES`Move the reCAPTCHA badgeon, bottom rightthe invisible v2 widget`BOT_SHIELD_RECAPTCHA_BADGE`Hide the reCAPTCHA badge**off**`BOT_SHIELD_RECAPTCHA_HIDE_BADGE`Google terms notice**off**, forced on by hiding the badge`BOT_SHIELD_RECAPTCHA_SHOW_TERMS`Honeypoton`spatie/laravel-honeypot`, and the blade component`BOT_SHIELD_HONEYPOT_ENABLED`Honeypot config driven from this packageon`BOT_SHIELD_HONEYPOT_MANAGE_CONFIG`Probe path firewallonthe `BlockProbePaths` middleware registered`BOT_SHIELD_PROBE_PATHS_ENABLED`Refuse denied crawlers' page views**off**the `DenyAgents` middleware registered`BOT_SHIELD_DENY_PAGE_VIEWS`robots.txt stanzason demandnone, run `bot-shield:robots`Event recordingonthe migration published and run`BOT_SHIELD_MONITORING_ENABLED`Hashed addresses in the events table**off**`BOT_SHIELD_MONITORING_HASH_IPS`Structured captcha loggingon`BOT_SHIELD_CAPTCHA_LOG`The optional Composer packages are only needed by the feature that uses them: `spatie/laravel-honeypot` for the honeypot, `jaybizzle/crawler-detect` for that detector driver, and `livewire/livewire` for the Livewire attributes. Enable a feature without its dependency and the package tells you which command to run rather than failing with a class-not-found.

### Exception hardening only

[](#exception-hardening-only)

The leanest useful install. No extra Composer packages, no migration, no middleware:

```
BOT_SHIELD_HONEYPOT_ENABLED=false
BOT_SHIELD_RECAPTCHA_ENABLED=false
BOT_SHIELD_MONITORING_ENABLED=false
BOT_SHIELD_RATE_LIMIT_ENABLED=false
BOT_SHIELD_PROBE_PATHS_ENABLED=false
```

Configuration
-------------

[](#configuration)

Full documentation lives in the published `config/bot-shield.php`. The top-level keys:

KeyDefaultDescription`enabled``true`Master switch. Turns every feature off at once.`detector``user-agent`Detector driver, or a `BotDetector` class name.`agents.rules`~40 rulesOrdered user agent rules, first match wins.`forms.enabled``true`Whether the form guard runs at all.`forms.use_detector``false`Also refuse anything the detector flags, not only `block` rules.`probe_paths.enabled``true`Whether `BlockProbePaths` answers scanner paths.`probe_paths.paths`scanner listPaths answered immediately.`rate_limit.attempts``5`Submissions allowed per address per form.`rate_limit.decay_seconds``60`Window for the above.`crawlers.deny_page_views``false`Whether `DenyAgents` refuses `deny` agents.`monitoring.enabled``true`Whether events are recorded to the table.`monitoring.hash_ips``false`Store a SHA-256 of the address instead of the address.`monitoring.retention_days``30`Pruning window for `model:prune`.`honeypot.enabled``true`Whether the honeypot fields render and are checked.`honeypot.manage_spatie_config``true`Drive spatie's config from this file.`honeypot.seconds``2`Minimum seconds between rendering and submitting.`captcha.enabled``true`Whether tokens are verified.`captcha.driver``google-v3``google-v3`, `google-v2` or `null`.`captcha.score``0.6`Minimum v3 score to accept.`captcha.score_challenge``0.9`Stricter threshold for `challenge` agents.`captcha.fail_open``false`Accept submissions when the provider is unreachable.`captcha.report_outages``false`Report an unreachable provider to your error tracker.`captcha.log``true`Log every verification with its score.`captcha.log_channel`app defaultChannel the above writes to.`captcha.badge``bottomright`Badge position on the invisible widget, or `inline`.`captcha.hide_badge``false`Hide the corner badge and show the notice instead.`captcha.show_terms``false`Render the notice on its own.`exceptions.enabled``true`Whether exception hardening is active.`exceptions.paths``livewire/*`Paths the hardening applies to.`exceptions.rules`Livewire setException matchers, extensible per site.Two notes worth reading before you deploy:

- **`honeypot.manage_spatie_config`** pushes this package's honeypot settings into spatie's config at boot, which is the point: one file per site instead of a published `honeypot.php` drifting per project. If you already maintain `config/honeypot.php` by hand, set it to `false`.
- **`monitoring.hash_ips`** keeps grouping and counting working, so repeat offenders are still visible, but you lose the ability to read an address off and act on it. Consider it if storing visitor addresses for a month is more than your privacy notice covers.

Artisan commands
----------------

[](#artisan-commands)

CommandPurpose`bot-shield:install`Publish config, wire the handler, add env keys.`bot-shield:doctor`Check the wiring and configuration.`bot-shield:stats`Summarise what was blocked and suppressed.`bot-shield:scores`Score distribution and a suggested threshold.`bot-shield:robots`robots.txt stanzas for denied agents.Translations
------------

[](#translations)

English and Dutch ship with the package. Publish them to change the wording:

```
php artisan vendor:publish --tag="bot-shield-lang"
```

Every visitor-facing string the package produces comes from `bot-shield::messages`, so an application that cannot resolve that namespace shows raw keys on live forms rather than failing loudly. That is not hypothetical: a translation loader backed by a database, rather than the file loader Laravel ships, can return nothing for a namespaced package group. Publishing the lang files does not always help either, since the same loader may not read the published copies.

`bot-shield:doctor` reports this under `Translations`, and it is worth checking after install on any site with a custom translation setup.

Testing
-------

[](#testing)

```
composer test
```

That runs PHPStan, Pint, a 100% type coverage check and the Pest suite. Individual steps are available as `composer analyse`, `composer lint:check`, `composer test:types` and `composer test:unit`.

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for recent changes.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please report security vulnerabilities by email to  rather than via the public issue tracker.

Credits
-------

[](#credits)

- [Marshmallow](https://github.com/marshmallow-packages)
- [All Contributors](https://github.com/marshmallow-packages/bot-shield/contributors)

License
-------

[](#license)

The MIT License. Please see the [License File](LICENSE.md) for more information.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/be33d2624e24c516e73892b0929447cc762f3622c024ab8d0d2a59042e5d2c7f?d=identicon)[marshmallow](/maintainers/marshmallow)

---

Top Contributors

[![LTKort](https://avatars.githubusercontent.com/u/2412670?v=4)](https://github.com/LTKort "LTKort (30 commits)")

---

Tags

laravelmarshmallowbot-shield

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/marshmallow-bot-shield/health.svg)

```
[![Health](https://phpackages.com/badges/marshmallow-bot-shield/health.svg)](https://phpackages.com/packages/marshmallow-bot-shield)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M348](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
