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

Web Terminal Laravel Package

mwguerra/web-terminal

Secure web terminal for Laravel with Filament integration. Run whitelisted commands locally or over SSH, with interactive PTY sessions, enum-based permissions, reusable scripts, and full audit logging for safe, traceable execution.

View on GitHub
Deep Wiki
Context7

Getting Started

Version pin first — the two majors are not interchangeable. v2.x targets Laravel 12.x–13.x, Filament 5.x, Livewire 4.x, PHP 8.3+. If you're on Laravel 11 / Filament 4, you must use ^1.0. Getting this wrong wastes a debugging session on Livewire entanglement errors rather than a clear composer conflict.

composer require mwguerra/web-terminal:"^2.0"
php artisan terminal:install

The installer is the fastest path: it publishes config, publishes the migration (with optional tenant_id), optionally publishes Blade views, and offers to run migrate. Prefer the non-interactive form for CI/automation:

php artisan terminal:install --config --migration --migrate --page --panel=admin -n

Where to look first:

  1. config/web-terminal.php — this is the allowlist source of truth. Read it before anything else.
  2. app/Providers/Filament/*PanelProvider.php — plugin registration and navigation.
  3. app/Filament/Pages/Terminal.php — only present if you generated a custom page; this is where per-page allowedCommands(), workingDirectory(), and connection type live.

Minimum viable registration:

use MWGuerra\WebTerminal\WebTerminalPlugin;

public function panel(Panel $panel): Panel
{
    return $panel->plugins([
        WebTerminalPlugin::make()
            ->terminalNavigation(group: 'Tools', sort: 100),
    ]);
}

Your first real use case should be read-only. Do not start with ->allowAllCommands(). Start here:

WebTerminal::make()
    ->local()
    ->allowedCommands(['ls', 'pwd', 'cd', 'cat', 'grep', 'tail', 'git status', 'git log *'])
    ->workingDirectory(base_path())

Confirm the audit log writes on each execution (Terminal Logs resource at /admin/terminal-logs). Once you've verified logging, only then widen the allowlist command by command. This package's headline feature is the whitelist — treat it as the primary control surface, not an afterthought.

Implementation Patterns

Per-page terminals beat one global terminal. The plugin default page is a demo. In real apps you want multiple terminals with different blast radii, each scoped to a purpose:

// app/Filament/Pages/LogViewerTerminal.php
class LogViewerTerminal extends BaseTerminal
{
    protected static ?string $navigationLabel = 'Tail Logs';
    protected static ?string $slug = 'terminal/logs';

    protected function getTerminalConfig(): array
    {
        return [
            'allowedCommands' => ['tail -n *', 'grep * storage/logs/*'],
            'workingDirectory' => storage_path('logs'),
        ];
    }
}

Generate the skeleton rather than hand-rolling:

php artisan terminal:make-page LogViewerTerminal --panel=admin --key=logs --allow-secure-commands

Critical wiring step: once you generate a custom page, you must disable the plugin defaults or you get two "Terminal" nav items:

WebTerminalPlugin::make()->withoutTerminalPage()   // custom page exists
WebTerminalPlugin::make()->withoutTerminalLogs()   // custom resource exists
// or, drop plugin defaults entirely but keep services:
WebTerminalPlugin::make()->only([])

This bites everyone exactly once. Budget for it.

Embed terminals as schema components when the terminal is context, not a destination. E.g. a "Deploy" page where the terminal runs ./deploy.sh scoped to a release directory, or a tenant detail page where the terminal runs php artisan tenant:status {tenant-id}:

use MWGuerra\WebTerminal\Schemas\Components\WebTerminal;

public static function configure(Schema $schema): Schema
{
    return $schema->components([
        WebTerminal::make()
            ->local()
            ->allowedCommands(['php artisan tenant:status *'])
            ->workingDirectory(base_path())
            ->height('400px'),
    ]);
}

SSH for remote operations. This is where the package earns its keep — ops engineers don't need to VPN into every box to run systemctl status nginx or docker ps. Configure the SSH connection per terminal instance, keep the credential in env, and scope the allowlist tighter than you would for local, because a local terminal already has your app's permissions while an SSH terminal can reach anything the remote user can.

Interactive mode for long-running commands. artisan tinker, queue:work, reverb:start, and migrate interactive prompts don't work in the plain exec path. Use the interactive mode (PTY/tmux) for those. Practical day-to-day use: a "Queue Worker" terminal you open to spot-check a worker's output, or a tinker terminal for incident triage without SSH'ing in.

Scripts are the package's most underrated feature and the main workflow pattern I'd recommend: define a named sequence (cache:clear → config:cache → route:cache → queue:restart) with progress reporting, expose it as a one-click button in Filament, and let the allowed-commands list stay narrow while the script gives a compound operation. This decouples "what an operator can do" from "what an allowlist permits", which is exactly the right abstraction.

Multi-tenant. Install with --with-tenant and the migration adds tenant_id. Filament scopes the logs resource automatically when tenant resolution is configured on the panel. If you're SaaS and want to expose a terminal to tenant admins (carefully, with a very short allowlist and possibly per-tenant SSH), this is the intended path.

Treat TerminalPermission as the authorization model. Don't scatter canAccess() checks based on role strings — use the enum-backed permission checks the package exposes so your policies and the package stay in sync. Register terminal pages behind a Filament policy that returns false for anyone without an explicit role.

Gotchas and Tips

Stream mode is frozen and will be removed. It still works in 2.x but receives no updates; it moves to mwguerra/web-terminal-stream and will be dropped on the next major. If you're building anything new: use Classic mode. If you're on Stream today, pin your version and plan the migration now. Do not build new features on Stream.

"Allowed commands" is per-terminal-instance, not global. A common mistake is to assume config/web-terminal.php governs all terminals. It provides the default; the schema component's allowedCommands([...]) on each terminal overrides it. Audit every terminal definition in your codebase — grep for allowAllCommands() and allowedCommands before shipping.

Wildcards are shell-glob, not regex. git * allows git push --force origin main. tail -n * allows tail -n 999999 /etc/passwd if the working directory or path traversal allows it. The whitelist restricts the command, not the arguments. For anything destructive (rm, mv, chmod, chown, artisan migrate) either exclude it entirely or wrap it in a script with hardcoded arguments.

Publishing views + v2 upgrade: if you published Blade views, search for @entangle('...') and replace with $wire.entangle('...'). This is a silent break — the page renders but state doesn't sync, and you'll spend an hour wondering why $wire calls no-op.

Multi-panel apps fail silently without --panel=. terminal:install --page and terminal:make-page both require an explicit panel when more than one exists. Without it the command errors, but if you're piping through -n in a deploy script you may not see it. Always pass --panel=.

Migration is optional but the logs resource needs it. If you disable logging in config (or skip the migration), the Terminal Logs resource throws on load. Either commit to audit logging — which is the whole point of this package — or remove the resource from nav with ->withoutTerminalLogs().

Audit logging is your compliance story, not your security control. Logs record commands and output after execution. They don't prevent anything. Enable them, export to a SIEM, but keep the real guardrails on the allowlist and on Filament policy access. Also: output logs can contain secrets (env dumps, tokens printed by mistake) — apply retention policy and consider redaction on the output column.

Session timeout and disconnect-on-navigate are on by default; tune them. Long-running queue:work or a watch-and-react session may want a longer inactivity timeout. Conversely, a kiosk-mode terminal should have the timeout shorter. Config keys live under session.* in the published config.

Rate limiting is the difference between "an operator" and "an operator scripting for i in $(seq 1 10000)". Verify the limiter is enabled in config and matches your expected human-speed command cadence.

Rate-limit the SSH path more aggressively than local. Each SSH exec may spin up a connection; a runaway loop can DoS your remote host.

Testing in isolation: stand up a local terminal in a phpunit/Pest test via the Livewire component and assert that a non-allowlisted command returns a rejection before executing. This is the one test I'd insist on — it proves the allowlist is actually enforced end-to-end rather than just in the UI dropdown.

Extension points: the Blade views are the practical customization surface (styling, output rendering, copy behavior). For deeper changes — custom connections, script runners — subclass the base Terminal page and override the config/provider methods rather than forking. The package has a single maintainer and 30 stars; assume you may need to carry a patch. Keep your composer.json constraint tight (^2.0 with a floor like ^2.1) and prefer subclassing over forking so a patch can be upstreamed or replaced cleanly.

Honest assessment: this is a young, single-maintainer package solving a genuinely hard problem (safe shell access). The whitelist + audit pattern is sound and useful — for internal ops tooling, staging environments, and admin panels where the user base is your own team. It is not something to expose to external customers, and the "use at your own risk" warning in the README is not boilerplate. Keep it out of customer-facing panels, restrict Filament access via policy, and treat the allowlist as code that gets reviewed in PRs.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log