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

Terminal Laravel Package

recca0120/terminal

Run Artisan and shell commands directly in the browser with this Laravel web terminal package. It provides a configurable terminal UI, predefined command buttons, and real-time output, making it handy for local debugging, demos, or admin panels without SSH access.

Deep Wiki
Context7

Getting Started

1. Install (dev-only first).

composer require recca0120/terminal --dev

Install as a dev dependency initially. If you later need it on a staging/admin host, promote it to require intentionally rather than by accident.

2. Publish the config.

php artisan vendor:publish --tag=terminal
# or, if the tag doesn't exist in your version:
php artisan vendor:publish --provider="Recca0120\Terminal\TerminalServiceProvider"

Open config/terminal.php before you visit the route. This single file is your security surface: enabled, middleware/guards, route prefix, and any command allow/deny lists. Key names have shifted between releases — treat the published file as the source of truth for your installed version, not blog posts.

3. Lock it down before the first request.

  • Gate on environment: only enable when app()->environment(['local']) (or a dedicated staging) unless you have a real auth story.
  • Attach auth (and ideally a policy/gate) plus a restrictive middleware group.
  • Force HTTPS if it ever leaves localhost.

4. Visit the route. Default is usually /terminal (configurable via the route prefix). If you get a 404, run php artisan route:list | grep -i terminal — the service provider may not be auto-discovered, or the route is disabled by config.

5. First use case — verify the runtime, not just the UI. From the browser terminal run:

php artisan about
php artisan route:list --except-vendor

That confirms which PHP binary, which .env, and which working directory the spawned process actually sees. If those three match your expectations, everything else is downhill.

Where to look first when something breaks: config/terminal.php → the service provider (route + middleware registration) → storage/logs/laravel.log → your web server's error log. The browser terminal's own error output is often swallowed by the streaming layer, so the server log is the reliable one.

Implementation Patterns

Environment-gated enablement (the default posture).

// config/terminal.php
'enabled' => env('TERMINAL_ENABLED', false),
'middleware' => ['web', 'auth', 'can:use-terminal'],
// AppServiceProvider::boot()
Gate::define('use-terminal', fn ($user) => $user->isAdmin()
    && app()->environment(['local', 'staging']));

The env var makes it trivially switchable per-host without a deploy, and the Gate means a compromised session on a non-admin account gets nothing.

IP allowlist middleware for staging/admin boxes.

class TerminalIpGuard
{
    public function handle($request, Closure $next)
    {
        abort_unless(in_array($request->ip(), config('terminal.allowed_ips', []), true), 403);
        return $next($request);
    }
}

Prepend it to the package middleware array. This is the cheapest meaningful defence when the terminal lives somewhere other than localhost.

Command allowlisting for team-facing installs. If you hand a terminal to non-senior devs or support staff, restrict to a known-safe set (php artisan migrate:status, cache:clear, queue:restart, route:list). Check the config for whatever the allowlist key is in your version; if it only supports a blacklist, invert the logic by wrapping the route in a middleware that inspects the posted command. Never rely on a blacklist — php -r, composer, and shell redirection all bypass naive string matching.

Long-running processes: detach, don't block.

queue:work, horizon, npm run dev, and artisan serve will tie up the request and eventually hit max_execution_time / FPM request_terminate_timeout. Run them detached and tail the log in the terminal instead:

nohup php artisan queue:work --tries=3 > storage/logs/queue.log 2>&1 &
tail -f storage/logs/queue.log

Use the terminal for observing long-running work, not for hosting it.

Audit logging via route middleware. Wrap the terminal route with a middleware that records user id, IP, and timestamp on entry (the package won't do this for you in most versions):

Log::channel('terminal')->info('terminal.session', [
    'user' => auth()->id(), 'ip' => $request->ip(), 'ua' => $request->userAgent(),
]);

Pair it with a retention policy on that channel — shell history is sensitive.

Custom Artisan commands as terminal "shortcuts". The best use of the terminal is running your own domain commands (php artisan app:resync-orders --since=...) rather than raw shell. Write the dangerous logic in a proper command with validation and confirm() guards, then let the terminal be the trigger. You get testability and logging for free.

Admin panel integration. Link to the terminal from Filament/Nova, or embed it in an iframe behind the same auth. Keep it on a separate route group with its own middleware so panel-level rate limiting doesn't interfere with streaming output.

Composer and asset work on remote boxes. The genuinely high-value day-to-day case: composer install --no-dev, php artisan migrate --force, php artisan config:clear on a host where SSH is awkward. Always run config:cache from SSH (or a proper deploy pipeline) afterwards — see the gotchas.

Gotchas and Tips

The spawned process is not your shell. It inherits the PHP-FPM user's environment, not your login shell. Expect missing PATH entries (~/.local/bin, nvm, asdf), no COMPOSER_HOME, and a different HOME. Symptom: "command not found" for something that works over SSH. Fix by using absolute paths or exporting in the command itself: PATH=$PATH:/usr/local/bin composer install.

No TTY means interactive prompts hang. Anything that asks a question (artisan migrate without --force in production, migrate:fresh, composer scripts, npm init) will sit waiting until the request times out, with no visible prompt. Always pass --no-interaction / -n and --force where appropriate, or run the interactive parts over real SSH.

Timeouts stack. The browser terminal can die from any of: max_execution_time, FPM request_terminate_timeout, nginx fastcgi_read_timeout / proxy_read_timeout, and any reverse-proxy idle timeout. If output stops mid-stream but the command clearly ran, it's almost always nginx buffering or gzip, not the package. Disable buffering and compression for the terminal route:

gzip off;
proxy_buffering off;      # or fastcgi_buffering off;

Session locking blocks concurrency. With the default file or database session driver, Laravel locks the session for the duration of a request. A long-running terminal command can lock you out of the rest of the app in the same browser. Either keep commands short or give the terminal route a separate session/cookie configuration. This one bites people in production and is genuinely confusing to diagnose.

Running as www-data with www-data's permissions. Files created by the terminal (storage/, cache, generated migrations) are owned by the web user. Then SSH deploys fail with permission errors. Prefer commands that write only where the web user should write, and be ready to chown after a terminal-created file.

Interactive TUIs (vim, top, htop, tinker REPL) are unreliable. The package proxies a stream, not a PTY, so full-screen applications render garbage or freeze. Use it for line-oriented commands and --format output.

ANSI escapes and control characters. Commands emitting colour/progress bars (composer, npm) can produce unreadable output if the frontend doesn't emulate a terminal properly for your version. Add --no-ansi / --no-progress when output looks mangled.

config:cache breaks env-driven toggles. Once config is cached, TERMINAL_ENABLED changes in .env do nothing until php artisan config:clear. If the terminal won't disappear after you disabled it, that's why — check bootstrap/cache/config.php.

cd doesn't persist. Each command runs in a fresh process rooted at base_path(). Chain commands or use absolute paths rather than expecting directory state.

Orphaned processes have no kill switch. Close the browser tab and a detached queue:work keeps running. Prefer Supervisor/systemd for anything long-lived, and know how to ps aux | grep artisan and kill when a stray worker starts eating CPU.

Audit and cleanup. By default, executed commands may only appear in the web server access log — not your application logs. If the terminal is available to more than one person, add the audit middleware above and decide retention before an incident, not after.

Octane and long-lived workers. If the host app runs under Octane/Swoole, streaming endpoints can behave differently or leak per-request state. Test the terminal under the same runtime as production before declaring it "fine".

Treat any exposed instance as remote code execution. The package is, definitionally, an RCE endpoint for anyone who reaches it. Minimum bar for anything non-local: real authentication, a Gate/policy, IP restriction, HTTPS, and an audit trail. When in doubt, keep it in require-dev and never ship the route to production.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk