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

laravel/agent-detector

Laravel Agent Detector is a lightweight PHP 8.2+ utility for detecting whether your code runs inside an AI agent or automated development environment, with known-agent checks and a simple standalone detectAgent() helper.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the shape of the API

composer require laravel/agent-detector   # requires PHP 8.2+

Note this is not a typical Laravel package: there is no service provider, no facade, no published config file, and no Blade directives. It is a framework-agnostic static utility that gets pulled in and called directly. Don't go looking for config/agent-detector.php — it doesn't exist.

One call gives you everything:

use Laravel\AgentDetector\AgentDetector;
use Laravel\AgentDetector\KnownAgent;

$result = AgentDetector::detect();

$result->isAgent;          // bool — am I inside *any* recognized agent?
$result->name;             // string — the detected agent's name
$result->knownAgent();     // ?KnownAgent — null when it's a custom/unknown agent

There is also a standalone function if you're in a non-Laravel context (a Composer script, a standalone tool in bin/, a Pest bootstrap):

use function Laravel\AgentDetector\detectAgent;

Where to look first

  1. The Supported Agents table in the README is the actual specification — the entire package is a lookup table from environment variables (plus one filesystem check) to a KnownAgent enum case. Read the table, not the source.
  2. AgentDetector::AGENT_ENV_VARS is a public constant (made visible in v2.0.1). This is your primary extension point and the fastest way to answer "what exactly is being checked?" at runtime — dump it in php artisan tinker.
  3. The changelog, not the README, is where new agents land (v2.0.4 alone added Kimi, Grok, Cline, Goose, Junie, and OpenClaw). Check it on every minor bump before you decide your detection is broken.
  4. KnownAgent::label() (added in v2.0.1) — use this for anything human-facing rather than hardcoding enum case names.

Your first use case: stop prompting a robot

The single highest-value day-one integration is making interactive Artisan commands non-interactive when an agent is driving:

public function handle(): int
{
    if (AgentDetector::detect()->isAgent) {
        // Agents can't answer a confirm() prompt; assume the safe default.
        $this->line('Agent environment detected — running non-interactively.');
        $this->call('migrate', ['--force' => true]);
    }
}

That alone removes a whole class of "the agent hung for 120 seconds and then timed out" failures.

Implementation Patterns

Resolve once, inject everywhere

detect() is cheap (a handful of env lookups), but calling it scattered across controllers, commands and jobs creates duplication and inconsistency. Bind it once and treat it as read-only request context:

// AppServiceProvider::register()
$this->app->singleton(AgentResult::class, fn () => AgentDetector::detect());

Then inject AgentResult (or a thin AgentContext wrapper of your own) into commands, jobs, and services. A singleton is correct here because the environment cannot change mid-process — it is not a per-request value.

Log and tag by agent identity

Use KnownAgent->value for machine tokens (stable, greppable, safe for telemetry dimensions) and label() for humans. Always include a fallback path for custom agents:

$result = AgentDetector::detect();

$agentTag = $result->knownAgent()?->value ?? ($result->isAgent ? 'custom' : 'human');

Log::withContext(['agent' => $agentTag]);

That context line pays for itself the first time a weird bug turns out to only reproduce under Claude Code or Cursor.

Determinism guards in seeders, factories, and fixtures

Agents run your test suite and your db:seed over and over. Seeding with random_int() or now()-derived values produces churn that pollutes diffs the agent then "fixes" incorrectly. Gate randomness behind detection:

$faker->seed(AgentDetector::detect()->isAgent ? 1234 : random_int(1, PHP_INT_MAX));

The same applies to snapshot tests, generated fixture files, and anything that writes timestamps into committed files.

Output hygiene for agent-consumed commands

An agent parsing your command output wants machine-readable, low-noise text. Detect and adapt:

  • Disable ANSI/progress animations (--no-ansi, or conditionally skip $this->progressBar()).
  • Prefer --format=json output that you can default-on when detected.
  • Skip browser-opening side effects ($this->openInBrowser(), dump(), interactive table pagination).
  • Set --no-interaction semantics at the command level rather than relying on Laravel's global flag.

Distinguish "agent" from "CI"

These overlap but are not the same question, and conflating them is a common source of wrong branches. Keep a separate predicate:

$isCi    = (bool) env('CI');
$isAgent = AgentDetector::detect()->isAgent;

// Agent ≠ CI: a dev running Claude locally on their laptop is not CI,
// and a CI pipeline running a script is not an agent.

Use $isAgent to change interaction style and $isCi to change credential/store selection.

Custom agents via AI_AGENT

If you're standardizing an internal agent, a CI image, or a bespoke script runner, set AI_AGENT=my-custom-agent and it's detected — isAgent === true with knownAgent() === null. Prefer this over forking the package or hardcoding your own env-var check. It also gives you a testable switch in development: run AI_AGENT=my-agent php artisan whatever to reproduce agent-mode behavior on your own machine, no agent required.

Testing detection without an agent

Set the env var in phpunit.xml per-suite, or override it in a test. The package reads the process environment, so:

public function test_command_runs_non_interactively_for_agents(): void
{
    putenv('AI_AGENT=test-agent');
    $_SERVER['AI_AGENT'] = $_ENV['AI_AGENT'] = 'test-agent';

    $this->artisan('your:command')
        ->expectsOutputToContain('Agent environment detected')
        ->assertSuccessful();
}

If the flag doesn't flip, set $_SERVER as well as putenv — different readers consult different sources, and that's the usual culprit.

Gotchas and Tips

Check isAgent before knownAgent() — always. knownAgent() is null for custom agents, and a large fraction of real detections are custom or AI_AGENT-based. Code that branches only on the enum silently treats real agent runs as human runs. Write if (! $result->isAgent) first, then switch on identity.

Never match on KnownAgent without a default arm. The enum grows with every minor release (six new cases in v2.0.4). A match ($knownAgent) will start throwing UnhandledMatchError the day you composer update and an agent you didn't know about appears. Use default =>, or match (true), or a foreach/array lookup. This is the single most likely way this package breaks your app.

AI_AGENT is overloaded, and ordering matters. It serves three jobs: an arbitrary custom-agent value, AI_AGENT=github-copilot / github-copilot-cli for Copilot, AI_AGENT=v0 for v0, and — since v2.0.2 — a Claude prefix match. So AI_AGENT=claude-something resolves to Claude, not "custom", and AI_AGENT=copilot-ish may not do what you expect. If you're inventing a custom value, don't pick something that prefixes an existing agent name.

Don't cache detection in config. The tempting move — 'is_agent' => AgentDetector::detect()->isAgent in a config file — breaks under php artisan config:cache. The value is frozen at cache-build time (often during a Docker build or a deploy step), so a config cached on your laptop can permanently report "not an agent" inside the agent's runtime. Read the environment at call time.

Detection reflects the process that started your server, not the one making the request. If an agent runs php artisan serve, the built-in PHP server inherits the agent's env vars and detection works. If the agent just sends HTTP requests to a server you started in another terminal, your app sees a totally normal request — there is nothing to detect. Detection is about the process, not the wire. This surprises people constantly.

This is a heuristic, not a security boundary. Every detection path except the Devin filesystem check is a user-settable environment variable. Anyone can set AI_AGENT=x and flip it. Never gate authorization, billing, rate limits, destructive-operation approval, or "trusted mode" on it. It's a UX switch and an observability tag.

Filesystem-based detection needs the filesystem. Devin is detected via /opt/.devin existing. Inside a container that doesn't bind-mount it, detection simply won't fire — and conversely, a developer who happens to have that path will be misdetected. Don't add more filesystem checks without accounting for both directions.

No service provider means no auto-discovery magic. There is nothing to disable, nothing to publish, and no extension points beyond what's public. Your options for extending are: (a) read AgentDetector::AGENT_ENV_VARS and add your own checks alongside it, or (b) maintain a small app-level enum for internal agents and map from AgentResult. Keep these in your own namespace — forking means re-merging a table that changes every few weeks.

Pin ^2.0 and watch for the namespace change. v2.0.0 renamed everything from AgentDetector\ to Laravel\AgentDetector\, and AgentResult::__construct no longer takes $isAgent (it's a public readonly property now). Any tutorial or LLM-generated snippet using the old namespace is v1-era garbage — this is a package agents will happily "help" you write wrong.

Long-running workers are fine; per-request mutation is not. Under Octane or a queue worker the result is stable for the process lifetime, so resolving it once is safe. What is not safe is writing to the AgentResult or stashing request-specific data on a singleton keyed by agent. Keep the singleton immutable.

Zero dependents means you're the QA. With essentially no downstream users, obscure platform quirks (a new agent shipping next week, a Copilot env-var rename, a Windows path assumption) will surface in your app first. If you rely on this for a hard behavioral branch, add a test that asserts your fallback path works when detection returns an unknown agent — that's the case most likely to arrive without warning.

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.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections