Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Agent Detector Laravel Package

ergebnis/agent-detector

Detect whether your PHP code is running under an AI agent. This Composer package checks environment variables for 30+ agents like Claude Code, Codex, Cursor, and Copilot, exposing a simple Detector class with one isAgentPresent() method.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package — there is no service provider, facade, config file, or auto-discovery:

composer require ergebnis/agent-detector

Requires PHP 7.4+ (through 8.6), so it drops into Laravel 8+ without friction.

Where to look first: the single class Ergebnis\AgentDetector\Detector and the README's agent/environment-variable table. The entire public API is one method:

use Ergebnis\AgentDetector;

$detector = new AgentDetector\Detector();

$isAgentPresent = $detector->isAgentPresent(\getenv());

Note the signature: it takes an array of environment variables as an argument rather than reading globals itself. That is the whole design — it is a pure-ish predicate over ['KEY' => 'value']. Everything else in this assessment follows from that.

First use case: make interactive Artisan commands safe to run under an agent. Agents (Claude Code, Codex, Cursor, Copilot CLI, Gemini CLI…) run your php artisan commands non-interactively and often can't answer questions or parse ANSI-decorated, progress-bar-laden output. The quickest win is a base command that refuses to prompt and switches to plain/JSON output when an agent is detected.

Minimal wiring in app/Providers/AppServiceProvider.php:

use Ergebnis\AgentDetector\Detector;

public function register(): void
{
    $this->app->singleton(Detector::class, fn () => new Detector());
}

Then anywhere:

$detector = app(Detector::class);
$isAgent = $detector->isAgentPresent(getenv());

Implementation Patterns

Wrap it behind your own contract

The class is final, so don't try to Mockery it or subclass it. Define a tiny interface and bind the package to it — this is what makes the rest of your codebase testable:

namespace App\Support;

interface AgentDetector
{
    public function isAgentPresent(): bool;
}
namespace App\Support;

use Ergebnis\AgentDetector\Detector;

final class EnvAgentDetector implements AgentDetector
{
    public function isAgentPresent(): bool
    {
        return (new Detector())->isAgentPresent(getenv());
    }
}
// AppServiceProvider::register()
$this->app->singleton(AgentDetector::class, EnvAgentDetector::class);

Now tests can bind a trivial fake, and if you later want to add a X-Client-Agent header check or a --agent CLI flag, you only touch the decorator.

Make Artisan commands agent-aware

Create a base command that centralises the policy, then extend it:

abstract class AgentAwareCommand extends Command
{
    protected function agentPresent(): bool
    {
        return $this->laravel->make(AgentDetector::class)->isAgentPresent();
    }

    protected function configureOutput(): void
    {
        if ($this->agentPresent()) {
            $this->output->setDecorated(false);
            $this->input->setInteractive(false);
        }
    }
}

Typical policy decisions inside commands:

  • Skip confirmation prompts ($this->confirm() returning false under an agent is a silent footgun — short-circuit it explicitly).
  • Emit --format=json output rather than tables, spinners, or ProgressBar.
  • Downgrade verbose logging so an agent doesn't burn its context window on noise.
  • Refuse destructive commands (migrate:fresh, db:wipe, cache clears on production) unless an explicit opt-in flag is passed.

Middleware for HTTP-facing behaviour

Agents browsing your app produce unusual traffic (no JS, rapid navigation, Accept headers skewed). A middleware that tags requests gives you a lever for debugging/telemetry:

public function handle(Request $request, Closure $next)
{
    if (app(AgentDetector::class)->isAgentPresent()) {
        Log::withContext(['is_ai_agent' => true]);
        // e.g. disable the debug toolbar, skip CSRF-token-heavy onboarding,
        // or serve a machine-readable variant of a docs page
    }

    return $next($request);
}

Keep this cheap: resolve the detector once (singleton), don't re-read the environment per request in a hot loop.

Guard AI-costing and side-effecting jobs

If queued jobs call a paid LLM API, write to a third-party service, or post to Slack, add an escape hatch:

public function handle(AgentDetector $detector): void
{
    if ($detector->isAgentPresent() && ! config('services.llm.allow_from_agents')) {
        $this->release(60);
        return;
    }

    // ... real work
}

This is particularly valuable in CI, where agents routinely shell out and trigger jobs you didn't intend to run twice.

Testing strategy

Because isAgentPresent() takes an array, the cleanest test is against the real class with a fixture array — no environment mutating at all:

$detector = new Detector();

$this->assertTrue($detector->isAgentPresent(['CLAUDECODE' => '1']));
$this->assertTrue($detector->isAgentPresent(['AI_AGENT' => '1']));
$this->assertFalse($detector->isAgentPresent([]));

For feature tests, bind your AgentDetector contract to a fake returning true/false. Prefer that over putenv(), which leaks between tests and is unreliable under PHPunit's process isolation settings.

Generic escape hatch for your own tooling

The package documents AI_AGENT as a generic "an agent is here" marker. Use it deliberately: set it in your own scripts or CI jobs to exercise the agent code paths without installing Claude Code or Cursor.

Gotchas and Tips

  • getenv() returns false on missing keys, and the array form can be empty. getenv() with no argument returns an array in modern PHP, but under some SAPIs or if variables_order in php.ini omits E, $_ENV is empty. If detection behaves strangely, dump getenv() and $_ENV and compare — $_SERVER usually holds the same variables and is more reliable under PHP-FPM.
  • PHP-FPM clear_env = on (the default in many distro configs) strips inherited environment variables from workers. An agent-detection check that works in CLI may silently return false over HTTP-FPM. Do the detection in the CLI/queue context unless you've explicitly allowed the variables through env[...] in the pool config.
  • config:cache and env(). Never put env('CLAUDECODE')-style lookups or a cached boolean in a config file and expect it to reflect a runtime-spawned agent. Call getenv() at the moment of decision, or bind a singleton that reads it at boot.
  • Presence only, not identity. isAgentPresent() returns bool. It does not tell you which agent, and there is no agentName(). If you need the specific agent (e.g., "Cursor users get X"), roll a small mapper over the README's table yourself — the column values are the contract.
  • Not a security boundary. Any process can set AI_AGENT=1 or CLAUDECODE=1. Never use this to gate authorisation, rate limits, or "trusted" code paths. It's a UX/operational heuristic only, and it fails closed in the wrong direction (an unset variable looks like a human).
  • Detection is env-var-based, so it's transitive. If your Laravel app is invoked by an agent via php artisan, the variables are inherited and detection fires. If a subprocess is spawned with a scrubbed environment (env -i, some Docker exec calls), it won't. That's a feature for blast-radius control, but it surprises people.
  • Suppress interactivity explicitly. Laravel's $this->input->isInteractive() isn't automatically false just because an agent is running. Set it yourself, or agents will hang on $this->ask() until a timeout kills the job.
  • Turn off decoration. $this->output->setDecorated(false) plus --no-ansi keeps logs parseable. Also consider --no-progress-style flags on any long-running command you expose.
  • Version pin tightly. Support for new agents lands in minor releases (COPILOT_CLI and PI_CODING_AGENT arrived in 1.2.0; Cursor's CURSOR_EXTENSION_HOST_ROLE in 1.1.0). Pin ^1.3 and let Dependabot pull new agent coverage in; don't pin to an exact patch or you'll miss the next agent entirely.
  • Zero maintenance cost, near-zero risk. The package has no runtime dependencies, no Laravel coupling, and no config. The only real extension point is your wrapper — keep the package usage confined to a single adapter class so a future swap (or a hand-rolled in_array check) is a one-file change.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo