Shell Execution
Two deliberately different trust tiers, not one capability with a toggle.
shellExec (shipped) is a small, explicit binary allowlist for apps that
know in advance which commands they need — a Git GUI, a media tool wrapping
ffmpeg. shellAgent (planned) is open-ended execution for apps that
can't enumerate commands ahead of time, like an AI coding assistant — and
because it has no allowlist doing the safety work, it needs a fundamentally
different guardrail.
shellExec — scoped exec (shipped)
capabilities: { shellExec: { allow: ['git', 'ffmpeg'] } }import { shell } from '@glyx-dev/react'
const { stdout, exitCode } = await shell.run('git', ['status', '--porcelain'])binmust exactly match anallowentry — no globs, no PATH search (resolved to an explicit path before spawning, closing PATH-hijacking on shared machines).- Arguments are always passed as a real argv array via the OS process API —
never through a shell interpreter (
sh -c/cmd /c). That's the actual injection defense; metacharacter rejection on top is defense-in-depth, not the primary guard. - 30s timeout, 8 MiB per-stream output cap.
See the capabilities reference for the full guardrail list.
Streaming execution (shell.spawn/shell.poll) is designed but not
implemented. shell.run buffers all output and resolves once the
process exits — fine for short commands, not for a long-running ffmpeg
transcode where you want progress as it happens.
shellAgent — open-ended exec (planned)
capabilities: { shellAgent: { scopeDir: './workspace' } }For apps that decide what to run at runtime rather than declare it ahead of time. No binary allowlist — any command runs — so the guardrail moves from which binaries to blast radius:
- Every spawned process's cwd is hard-scoped to
scopeDir(canonicalized;../absolute-path escapes rejected before spawn, not just discouraged). Shipped — the capability declaration and cwd-scoping enforcement already exist inglyx-security. - Not yet built: a native, JS-independent activity overlay that shows
every command as it runs, drawn directly by the renderer rather than the
app's own JS. This is a hard requirement for
shellAgentto ship, not optional polish — without an unsuppressable view of what's actually executing, the capability would just be a trust-me boolean with no runtime accountability. It reuses the same "native chrome survives even if the app's JS misbehaves" principle already used elsewhere in Glyx: adecorations: falsewindow whose JS crashes before rendering its own titlebar gets a native (not JS-drawn) fallback close control, so it's never strandable — the same idea, applied to command visibility instead of window chrome. - Not yet built: the JS-facing
shellAgentbinding itself (currently onlyshellExec's allowlisted path is wired up).
Do not declare shellAgent in a real app yet — the capability check
exists, but nothing enforces the native-overlay requirement, so there is
currently no way to actually satisfy the trust model this tier is meant
to provide.
Why two tiers instead of one
An app declaring "I need to shell out to ffmpeg" and an app declaring "I
need to run arbitrary developer commands" are asking for wildly different
amounts of trust. A single boolean (or even a single allowlist field) can't
express that difference — either it's too permissive for the ffmpeg case or
too restrictive for the agent case. Both tiers spawn through the identical
primitive (std::process::Command, argv-only, never a shell interpreter);
shellAgent is shellExec with the allowlist removed and the native
activity overlay added, not a separate implementation.