The command lived in a narrow truncate'd grid cell, so anything but a short invocation was unreadable. It now gets its own scrollable code block, plus a breakdown of each variable (using the script's configured labels when available) and the value it resolved to for that run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TriggerShell
A CLI that launches a self-hosted web app for running your shell scripts: define scripts and their parameters in a config file, get a generated form UI, run them asynchronously with live streaming output, and keep a searchable history — all behind optional login, with a full REST/WS API for automation.
Features
- One config file defines the server, auth, and every script (command, working dir, timeout, and typed/validated variables with defaults and choices).
- Generated forms — each variable renders as the right control (text, number, slider, select, radio, checkbox, switch, multi-select) based on its type, with client- and server-side validation from the same schema.
- Safe execution — scripts run via argv arrays, never a shell string. User input can never break out into shell metacharacters.
- Live monitoring — stdout/stderr and status stream over WebSocket to an in-browser terminal view; cancel a run from the UI or the API.
- Run history — every run's variables (secrets redacted), resolved command, status, exit code, and full log are persisted in SQLite.
- Optional auth — built-in username/password sessions and API tokens, toggleable per config.
- REST + WebSocket API for scripting your script runner.
Quickstart
pip install triggershell # or: pip install -e . from a checkout
triggershell init # scaffold triggershell.yml (+ .env for secrets) in the current directory
triggershell users add admin # create a login (skip if you set auth.enabled: false)
triggershell dev # start in dev mode and open the browser
Edit triggershell.yml to add your own scripts (see Configuration below),
then run triggershell start for a production build.
Requirements
- Python >= 3.9
- Node.js >= 20 (checked by the CLI; not auto-installed)
- pnpm (auto-provisioned via Corepack if missing and Corepack is available)
Configuration
TriggerShell is driven entirely by one YAML file (default: ./triggershell.yml, override with
-c/--config or TRIGGERSHELL_CONFIG_PATH). Secrets referenced via ${VAR} (like
auth.sessionSecret) are meant to live in a .env file next to the config, not in the config
itself — triggershell init generates both. Minimal example:
server:
host: 127.0.0.1
port: 4173
auth:
enabled: true
sessionSecret: "${TRIGGERSHELL_SESSION_SECRET}" # >= 32 chars
users:
- username: admin
passwordHash: "${TRIGGERSHELL_USER_ADMIN_PASSWORD_HASH}" # from `triggershell users add`
tokens: []
database:
path: .triggershell/triggershell.db
logs:
dir: .triggershell/logs
retentionDays: 30
scripts:
- id: deploy-service
name: Deploy Service
command: bash
args: ["./scripts/deploy.sh"]
timeoutSeconds: 600
variables:
- name: environment
type: enum
choices: [staging, production]
default: staging
required: true
passAs: arg
argName: --env
- name: dryRun
type: boolean
default: false
passAs: flag
argName: --dry-run
A full, richly-commented example lives at examples/triggershell.yml
(with a matching .env.example), and the complete field-by-field
reference is in docs/CONFIG_REFERENCE.md.
Each variable's type (string / number / boolean / enum / multiselect) picks a sensible
default UI control; set control explicitly to override it (e.g. slider for a number, radio
for an enum). passAs (arg / flag / env / stdin) decides how the validated value reaches
the script — always as a discrete argv element or env var, never interpolated into a shell string.
CLI Usage
| Command | Description |
|---|---|
triggershell init [PATH] |
Scaffold a new config file + .env (--port, --auth/--no-auth, --force) |
triggershell validate [-c CONFIG] |
Validate a config file (fast Python pre-flight + full Node/Zod schema) |
triggershell dev [-c CONFIG] [--port] [--host] [--no-browser] |
Run in development mode (hot reload) |
triggershell start [-c CONFIG] [--port] [--host] [--no-browser] [--skip-build] |
Build (if stale) and run in production mode |
triggershell doctor |
Print environment/config diagnostics |
triggershell users add <username> [-c CONFIG] [--inline] |
Hash a password, store it in .env, and print a ${VAR} snippet for auth.users (--inline prints the raw hash instead) |
triggershell users add-token <name> [-c CONFIG] [--inline] |
Generate an API token, store its hash in .env, and print a ${VAR} snippet for auth.tokens (--inline prints the raw hash instead) |
Web App Guide
- Scripts (
/) — every configured script as a card; click through to its run form. - Run form (
/scripts/:id) — a generated form for the script's variables, validated before submit. - Run detail (
/runs/:id) — live streaming terminal output, status, exit code, and a Cancel button while the run is active. - History (
/runs) — every past run, filterable by script/status via the API (?scriptId=,?status=).
Authentication
Set auth.enabled: false for trusted/local-only use — the app then requires no login at all.
When enabled, sessions are signed/encrypted cookies (no server-side session store), and API
clients can instead send Authorization: Bearer <token> using a token from users add-token.
Passwords are hashed with argon2id; only the hash ever lives in the config file.
API Reference
Full reference with request/response shapes and curl examples: docs/API.md.
| Method | Path | Notes |
|---|---|---|
| GET | /api/healthz |
Unauthenticated readiness probe |
| POST | /api/auth/login |
{username, password} → sets session cookie |
| POST | /api/auth/logout |
Clears the session |
| GET | /api/auth/session |
Current auth state |
| GET | /api/scripts |
List configured scripts |
| GET | /api/scripts/:scriptId |
Full script schema (variables, UI hints) |
| POST | /api/scripts/:scriptId/runs |
Start a run: {variables: {...}} |
| GET | /api/runs |
History: ?scriptId=&status=&limit=&cursor= |
| GET | /api/runs/:runId |
Single run's status/metadata |
| POST | /api/runs/:runId/cancel |
Cancel an active run |
| GET | /api/runs/:runId/logs |
Full or tailed log output (?tail=&download=) |
| WS | /ws/runs |
Subscribe to a run's live output/status; send cancel |
Development
pnpm --dir app install
pnpm --dir app dev # or: triggershell dev, which wraps this
pnpm --dir app lint
pnpm --dir app typecheck
pnpm --dir app db:studio # browse the SQLite DB
Repo layout:
triggershell/ Python CLI (launcher/orchestrator only)
app/ Next.js app - all server logic (API, auth, script execution) lives here
examples/ A runnable example config + scripts
docs/ Config/architecture/API reference docs
See docs/ARCHITECTURE.md for how the pieces fit together.
Security Notes
- Scripts are always spawned with an argv array (
execa), never a shell string — variable values can never inject additional shell commands. A script's owncommand/argsmay still useshell: trueas an explicit, documented opt-in when the script genuinely needs pipes/globs; that reintroduces shell interpretation ofpassAs: argvalues, so preferpassAs: envfor anything user-controlled in that case. secret: truevariables are masked in the UI and redacted from persisted run records; only the live child process ever sees the real value.- The server binds to
127.0.0.1by default — setserver.hostexplicitly to expose it further. - Route Handlers check auth themselves (
requireAuth());proxy.tsis only a fast, optimistic redirect layer, not the security boundary. auth.sessionSecretshould live in.env, not the config file —triggershell initsets this up for you.passwordHash/tokenHashare one-way hashes (not the secrets themselves), so storing them directly in the config is reasonably safe — the same trust model as/etc/shadowor.htpasswd— buttriggershell users add/add-tokenstore them in.envvia${VAR}by default too, for cases where you don't want them readable by anyone with config access at all. Pass--inlineto get the old behavior of printing the raw hash to paste into the config.
License
MIT — see LICENSE.