Static analysis for Laravel environment configuration. Catches env() calls outside config/, example file drift, and possible committed secrets. Reports an isolation score and fails CI when thresholds are breached.
env() outside a config file is probably the single most common Laravel footgun. It works in development, then returns null in production the moment someone runs config:cache - silently, with no error, nothing in the logs.
Any env() call outside config/ returns null after caching. The kind of bug that passes code review and staging, then breaks production hours after a deploy.
New variables appear in production and never make it into the example file. Onboarding a new developer means discovering missing configuration one runtime error at a time.
.env.example is copy-pasted from a real .env file more often than anyone admits. A surprisingly common place for real credentials to end up in a public repository.
The scanner walks every PHP file using nikic/php-parser - real parse-tree traversal across heredocs, multiline calls, and dynamic arguments. Four independent detection categories, each configurable.
env() calls that live inside config/. Reported on every run. Fails CI when it drops below your threshold. Active bypasses are tracked separately so the score cannot be inflated by suppressing rather than fixing.
For cases where a direct env() call is genuinely necessary, two bypass forms are supported. Both require a documented reason. Both require an expiry date - once it passes, the bypass is reported as expired rather than honoured. "Temporary" exceptions cannot quietly become permanent.
use Phoenix1331\LaravelEnvAudit \Attributes\WithoutEnvAudit; #[WithoutEnvAudit( 'Tenant bootstrap needs TENANT_ID before config boots, ADR-012', expires: '2027-01-01' )] class TenantBootstrapProvider extends ServiceProvider { public function register(): void { $id = env('TENANT_ID'); } }
// env-audit-ignore: legacy queue worker // reads this before config boots, // ticket INFRA-5190 $driver = env('LEGACY_CACHE_DRIVER'); // Both forms are recorded in the // report's exclusions section. // Active bypass count is its own // metric - the score cannot be // inflated by suppressing findings.
- name: Run env audit run: php artisan env-audit:run --fail-on=direct-usage,possible-secret # JSON output for downstream tooling - name: Run env audit (JSON) run: php artisan env-audit:run --json > env-audit.json
Exits 0 when no --fail-on categories have findings, 1 otherwise. No application boot required - all analysis is static. No database connections, no service providers, no side effects.
Each solves one piece of this problem. None combine all three - and none produce a score, a gate, or a report you can attach to a PR.
| Capability | Larastan | env-sync | gitleaks | env-audit |
|---|---|---|---|---|
| Catch env() outside config/ | Partial | No | No | Yes |
| .env.example drift detection | No | Yes | No | Yes |
| Secret heuristics on example file | No | No | Git history | Yes |
| Isolation score across call sites | No | No | No | Yes |
| Configurable CI gate per category | No | No | No | Yes |
| Expiring bypass mechanism | No | No | No | Yes |
| Self-contained HTML report | No | No | No | Yes |