The Problem
Every Laravel developer knows the drill. Someone on the team needs to clear the cache, run migrations, or trigger a seeder — but they don't have SSH access. So they ping you on Slack. You SSH in, run the command, confirm the output, and go back to what you were doing.
Multiply that by a dozen teammates and a handful of environments, and it becomes a real productivity drain.
I built Laravel Artisan Runner to solve this. It's a package that lets you expose pre-approved Artisan commands through a clean, Livewire-powered web interface — with full audit logging, queued execution, and notifications baked in.
What It Does
At its core, Artisan Runner gives you three things:
- A web UI to browse, configure, and execute Artisan commands
- An allowlist system so only approved commands can be run
- A complete audit trail of every execution — who ran what, when, with what parameters, and what happened
Every command runs through a queued job. No blocking HTTP requests. No timeouts on long-running migrations.
Installation
composer require cleaniquecoders/laravel-artisan-runner
php artisan artisan-runner:install
php artisan migrate
That's it. Three commands and you're up.
The install command publishes the config, migrations, views, and assets in one shot. Then drop the component into any Blade view:
<livewire:artisan-runner::command-runner />
The default route is /artisan-runner, protected by web and auth middleware.
Configuration: Three Discovery Modes
The package supports three ways to determine which commands are available.
Manual (Default — Safest)
Only commands you explicitly list in allowed_commands are available:
// config/artisan-runner.php
'discovery_mode' => 'manual',
'allowed_commands' => [
'cache:clear' => [
'label' => 'Clear Cache',
'description' => 'Flush the application cache.',
'group' => 'Cache',
'parameters' => [],
],
'migrate' => [
'label' => 'Run Migrations',
'description' => 'Run database migrations.',
'group' => 'Database',
'parameters' => [
['name' => '--force', 'type' => 'boolean', 'label' => 'Force (production)', 'default' => false],
['name' => '--seed', 'type' => 'boolean', 'label' => 'Run seeders', 'default' => false],
],
],
],
Auto Discovery
Surfaces all Artisan commands minus your exclusion list:
'discovery_mode' => 'auto',
'excluded_commands' => ['down', 'up', 'serve', 'tinker', 'db:wipe'],
'excluded_namespaces' => ['make', 'schedule', 'queue', 'stub'],
Good for development. I wouldn't recommend this for production without a tight exclusion list.
Selection
A middle ground — only explicitly included commands are discovered, then merged with your manual entries:
'discovery_mode' => 'selection',
'included_commands' => ['cache:clear', 'migrate', 'config:cache', 'route:cache'],
There's also a CLI tool to help you build your command list:
php artisan artisan-runner:discover
php artisan artisan-runner:discover --output=json
php artisan artisan-runner:discover --dry-run
How It Works Under the Hood
The architecture follows a clean action-based pattern:
User clicks "Run"
→ Livewire CommandRunner component
→ RunCommandAction (validates against allowlist)
→ Dispatches RunArtisanCommandJob to queue
→ Job calls Artisan::call()
→ Output + exit code saved to CommandLog
→ Notification dispatched (if enabled)
The Audit Log
Every execution creates a CommandLog record:
CommandLog::create([
'uuid' => Str::uuid(),
'command' => 'migrate',
'parameters' => ['--force' => true],
'status' => 'pending', // pending → running → completed/failed
'ran_by_type' => 'App\Models\User',
'ran_by_id' => 42,
]);
The status lifecycle is simple:
pending → running → completed
→ failed
Each log tracks start time, finish time, full output, and exit code. The Livewire component polls every 5 seconds, so you see status updates in real time.
Query your logs programmatically:
use CleaniqueCoders\ArtisanRunner\Models\CommandLog;
use CleaniqueCoders\ArtisanRunner\Enums\CommandStatus;
// Recent failures
CommandLog::where('status', CommandStatus::Failed)->recent(7)->get();
// Who's been running commands
CommandLog::with('ranBy')->latest()->take(20)->get();
Smart Parameter Rendering
The UI automatically renders the right input type based on parameter definitions:
- Arguments (positional) → text inputs
-
Boolean flags (
--force,--seed) → checkboxes -
Numeric options (
--step=5) → number inputs - String options → text inputs
One gotcha I hit early: Livewire doesn't play well with wire:model bindings that have -- prefixes (like parameterValues.--force). The fix was to use index-based keys internally and map them back to parameter names when dispatching. Small detail, but it would've bitten anyone building this.
Notifications
When a command completes or fails, the package can notify a configurable user via any Laravel notification channel:
'notification' => [
'enabled' => true,
'channels' => ['database', 'mail'],
'notifiable' => [
'model' => \App\Models\User::class,
'identifier' => 'email',
'value' => env('ARTISAN_RUNNER_NOTIFY_EMAIL', 'ops@yourdomain.com'),
],
],
The notification includes the command name, status, exit code, and execution duration. Wire it up to Slack, Discord, or whatever your team uses.
Security: Built Paranoid by Default
This is a package that runs shell commands from a web interface. Security isn't optional.
Allowlist-first. The default mode is manual. Nothing runs unless you explicitly approve it.
Auth-protected routes. Default middleware is ['web', 'auth']. Tighten it further:
'route' => [
'middleware' => ['web', 'auth', 'role:admin'],
],
Dangerous commands pre-excluded. Even in auto-discovery mode, commands like down, db:wipe, migrate:fresh, tinker, and serve are excluded out of the box.
Full audit trail. Every execution is logged with who triggered it (polymorphic relation — works with any model, not just User).
No retries. Failed jobs stay failed. You investigate, you don't auto-retry migrate --force in production.
Timeout protection. Commands are killed after 300 seconds. No runaway processes.
Livewire 3 + 4 Compatibility
The package supports both Livewire 3 and 4. The service provider handles registration automatically:
if (method_exists(Livewire::class, 'addNamespace')) {
// Livewire 4
Livewire::addNamespace('artisan-runner', __DIR__ . '/Livewire');
} else {
// Livewire 3
Livewire::component('artisan-runner::command-runner', CommandRunner::class);
}
No feature flags. No config toggles. It just works.
When Should You Use This?
Great fit:
- Admin dashboards where non-technical staff need to trigger maintenance tasks
- Deployment pipelines where you want a "run post-deploy tasks" button
- Multi-tenant apps with per-tenant cache clearing or migrations
- Teams where not everyone has SSH access but everyone needs to clear caches
Not ideal for:
- Interactive commands (
tinker,make:model) - Long-running batch jobs that exceed 5 minutes
- Anything that needs real-time streaming output (it captures output after completion)
Stack Compatibility
| Supported | |
|---|---|
| PHP | 8.3+ |
| Laravel | 11, 12, 13 |
| Livewire | 3, 4 |
| Tailwind | 4 (via CDN) |
Try It Out
composer require cleaniquecoders/laravel-artisan-runner
php artisan artisan-runner:install
php artisan migrate
php artisan queue:work
Then visit /artisan-runner in your browser.
Source code: github.com/cleaniquecoders/laravel-artisan-runner
I'm Nasrul Hazim, a software engineer from Malaysia building Laravel packages and tools. Find more of my work at nasrulhazim.com.

Top comments (0)