shipfastlabs/agent-detector
Lightweight PHP utility that detects whether your code is running inside an AI agent or automated dev environment. Identify Claude, Cursor, Gemini, Codex and more via env vars or a standalone detectAgent() helper.
Install via Composer:
composer require laravel/agent-detector
First use case: Detect AI agents in your Laravel application's AppServiceProvider boot method or middleware:
use Laravel\AgentDetector\AgentDetector;
public function boot()
{
$result = AgentDetector::detect();
if ($result->isAgent) {
Log::info("Running in AI environment: {$result->name}");
// Customize behavior for AI agents
}
}
Key starting points:
AgentDetector::detect() - Core detection methoddetectAgent() - Standalone function aliasKnownAgent enum - Predefined agent types (as of v2.0.5: Claude, Copilot, Copilot cloud agent, Kimi, Grok, Cline, Goose, Junie, OpenClaw, ZCode, Qwen Code, Crush, Windsurf, Zed Agent, Kiro IDE, Jules, and earlier built-ins)AgentResult object - Contains detection resultsWhat's new in v2.0.5: detection for ZCode, Qwen Code, Crush, Windsurf, Zed Agent, Kiro IDE, Copilot cloud agent, and Jules was added — eight new enum cases in another patch release. If you were hand-rolling detection for any of these via
AI_AGENTor a custom detection method, you can drop that code. No breaking changes; existing detection and namespaces are unchanged. v2.0.4 added Kimi, Grok, Cline, Goose, Junie, and OpenClaw — the same migration advice applies if you skipped it.
// Basic detection
$result = AgentDetector::detect();
// Check for any agent
if ($result->isAgent) {
// Agent detected
}
// Check specific agent
if ($result->knownAgent() === KnownAgent::Claude) {
// Claude-specific logic
}
All fourteen new agents are first-class enum cases, so the same branching style works without custom detection code:
use Laravel\AgentDetector\KnownAgent;
$agent = AgentDetector::detect()->knownAgent();
$profile = match (true) {
$agent === KnownAgent::Claude,
$agent === KnownAgent::Kimi => 'long-context-assistant',
$agent === KnownAgent::Copilot,
$agent === KnownAgent::CopilotCloudAgent,
$agent === KnownAgent::Cline,
$agent === KnownAgent::Goose,
$agent === KnownAgent::Junie,
$agent === KnownAgent::Crush,
$agent === KnownAgent::QwenCode,
$agent === KnownAgent::ZCode => 'ide-or-terminal-coder',
$agent === KnownAgent::Windsurf,
$agent === KnownAgent::ZedAgent,
$agent === KnownAgent::KiroIDE => 'editor-embedded-agent',
$agent === KnownAgent::Grok,
$agent === KnownAgent::OpenClaw,
$agent === KnownAgent::Jules => 'general-agent',
default => 'unknown',
};
A practical day-to-day pattern is to group agents into behavioural families rather than checking each case inline:
public function isInteractiveCodingAgent(AgentResult $result): bool
{
return in_array($result->knownAgent(), [
KnownAgent::Copilot,
KnownAgent::CopilotCloudAgent,
KnownAgent::Cline,
KnownAgent::Goose,
KnownAgent::Junie,
KnownAgent::Crush,
KnownAgent::QwenCode,
KnownAgent::ZCode,
KnownAgent::Windsurf,
KnownAgent::ZedAgent,
KnownAgent::KiroIDE,
], true);
}
This survives future releases that add more coding-oriented agents far better than a long if/else chain — v2.0.5 is the second consecutive patch release to prove the point.
public function handle(Request $request, Closure $next)
{
$result = AgentDetector::detect();
if ($result->isAgent && $result->knownAgent() === KnownAgent::Copilot) {
return response()->json(['message' => 'Copilot detected'], 403);
}
return $next($request);
}
See the gotcha below before copying this: Copilot no longer covers every Copilot surface as of v2.0.5.
public function register()
{
$this->app->singleton('agent-detector', function () {
return AgentDetector::detect();
});
}
// Set environment variable
putenv('AI_AGENT=my-custom-agent');
// Or in .env
// AI_AGENT=my-custom-agent
// Then detect
$result = AgentDetector::detect();
if ($result->name === 'my-custom-agent') {
// Custom logic
}
Note: before registering custom detection for ZCode, Qwen Code, Crush, Windsurf, Zed Agent, Kiro IDE, Copilot cloud agent, Jules, Kimi, Grok, Cline, Goose, Junie, or OpenClaw, confirm the package doesn't already cover it (v2.0.5+ does for all of these). Duplicate detection methods are harmless but make debugging output noisier.
// In PHPUnit tests
public function testAgentDetection()
{
putenv('AI_AGENT=github-copilot');
$result = AgentDetector::detect();
$this->assertTrue($result->isAgent);
$this->assertEquals(KnownAgent::Copilot, $result->knownAgent());
}
public function testNewlySupportedAgentsAreRecognised()
{
foreach ([
'zcode' => KnownAgent::ZCode,
'qwen-code' => KnownAgent::QwenCode,
'crush' => KnownAgent::Crush,
'windsurf' => KnownAgent::Windsurf,
'zed' => KnownAgent::ZedAgent,
'kiro' => KnownAgent::KiroIDE,
'jules' => KnownAgent::Jules,
// v2.0.4
'kimi' => KnownAgent::Kimi,
'grok' => KnownAgent::Grok,
'cline' => KnownAgent::Cline,
'goose' => KnownAgent::Goose,
'junie' => KnownAgent::Junie,
'openclaw' => KnownAgent::OpenClaw,
] as $envValue => $expected) {
putenv("AI_AGENT={$envValue}");
$this->assertSame($expected, AgentDetector::detect()->knownAgent());
}
}
Adjust the raw env strings to the exact values the package matches on — the mapping above is illustrative; check KnownAgent / AGENT_ENV_VARS for the canonical identifiers and the exact enum case names (e.g. ZedAgent vs Zed, KiroIDE vs Kiro).
Environment Variables: Detection relies on environment variables. Ensure they're properly set in your deployment environment.
# Example for testing
AI_AGENT=github-copilot php artisan tinker
Namespace Changes: v2.0.0 changed namespace from AgentDetector to Laravel\AgentDetector. Update imports if migrating.
False Positives: Some environments might trigger multiple detection methods. Verify with AgentDetector::AGENT_ENV_VARS:
print_r(AgentDetector::AGENT_ENV_VARS);
Performance: Detection is lightweight but not free. Cache results if checking frequently:
$result = cache()->remember('agent-detection', now()->addHours(1), function() {
return AgentDetector::detect();
});
Exhaustive match on KnownAgent: v2.0.4 added six enum cases, v2.0.5 added eight more. Any match (KnownAgent::…) without a default arm — or a switch with no fallback — will now throw UnhandledMatchError / fall through for ZCode, Qwen Code, Crush, Windsurf, Zed Agent, Kiro IDE, Copilot cloud agent, Jules, and the v2.0.4 set. Always supply a default arm, and prefer in_array() family checks over enumeration.
Copilot is no longer "all Copilot": Copilot cloud agent is a distinct case from Copilot. Any check like === KnownAgent::Copilot or in_array($agent, [KnownAgent::Copilot]) silently stops matching the cloud agent after upgrading. If you mean "any Copilot", match both cases or gate on a family list.
Persisted enum values: If you store KnownAgent cases in a database, queue payloads, or cache keys, using ->name is safe; casting a stored string back to the enum for a new case is only safe after every consumer is on v2.0.5+. Rolling deploys can briefly mismatch — and note the v2.0.5 additions widen that window again.
Cached detection results mask new agents: because detection results are often cached (see #4), an app that upgraded to v2.0.5 can still report unknown for Qwen Code or Crush until the cache TTL expires. Invalidate the cache key on deploy if agent identity drives behaviour.
Inspect Detection Logic:
$result = AgentDetector::detect();
dd($result->name, $result->isAgent, $result->knownAgent());
Check All Environment Variables:
foreach (AgentDetector::AGENT_ENV_VARS as $var) {
echo "$var = " . getenv($var) . "\n";
}
Confirm You're on v2.0.5+: a missing KnownAgent::Jules or KnownAgent::QwenCode case (or a "class constant not found" error) usually means Composer resolved an older tag. Check with:
composer show laravel/agent-detector
Custom Detection: Extend the detector by creating a custom class:
class CustomAgentDetector extends AgentDetector
{
protected static function detectCustomAgent(): ?string
{
return getenv('MY_CUSTOM_VAR') ? 'my-custom-agent' : null;
}
}
KnownAgent Enum: Use label() method for human-readable names:
echo KnownAgent::Claude->label(); // "Claude"
The label is what you want in logs and UI — the raw name of the newer agents (e.g. OpenClaw, ZCode, KiroIDE) won't match how users write them.
AgentResult Properties:
isAgent: Boolean indicating if any agent detectedname: String name of detected agentknownAgent(): Returns KnownAgent enum if matches known agents — returns null for anything not in the enum, including custom AI_AGENT valuesEnvironment Variable Priority: The detector checks variables in a specific order. Newer agents may override older ones. Having added fourteen agents across two patch releases, environments with several agent variables set can resolve differently than they did on v2.0.3 or v2.0.4 — if a CI image or devcontainer sets more than one (common with IDE-embedded agents like Windsurf, Zed Agent, and Kiro IDE sharing a shell with a CLI coder), re-verify which one wins rather than assuming.
Editor-embedded vs terminal agents: v2.0.5 skews heavily toward editor/IDE-embedded agents. Those typically export their marker variables into the integrated terminal, so shell commands run from inside the editor can detect differently than the same command run from an external terminal. Worth pinning down in any behaviour that changes based on detection.
Add New Agent Detection:
// In a service provider
AgentDetector::addDetectionMethod(function() {
return getenv('NEW_AGENT_VAR') ? 'new-agent' : null;
});
Check the release first — a fair number of "custom" agents have been absorbed into core over time (six in v2.0.4, eight more in v2.0.5), and you'll want to remove your shim once it lands upstream.
Custom Agent Detection Class:
class MyAgentDetector extends AgentDetector
{
public static function detect(): AgentResult
{
$result = parent::detect();
if ($result->name === 'custom') {
// Custom logic
}
return $result;
}
}
Override Detection Logic:
// Monkeypatch for testing
AgentDetector::setDetectionMethods([...]);
When overriding, re-check the default method list after each upgrade — v2.0.5's additions mean a hardcoded list silently drops detection for ZCode, Qwen Code, Crush, Windsurf, Zed Agent, Kiro IDE, Copilot cloud agent, Jules, and the v2.0.4 set.
How can I help you explore Laravel packages today?