Repository navigation
[Bug] Shell-permission classifier kills the CLI: unhandled shell-quote "Bad substitution" error (1.84.0) #1043
Description
Summary
A shell command that contains a bash parameter expansion (${var%%pattern} / ${var##pattern}) makes the CLI process die. The throw happens inside the shell-permission classifier:
ShellPermissionPrompt → getShellPermissionChoices → shellPermissionRulesForCommand → classifyShellCommand → classify → shell-quote parse → parseEnvVar
Nothing catches it, so it surfaces as CRITICAL: Unhandled Promise Rejection! and the TUI closes. Work is not lost (writes completed before the crash stay on disk), but the session ends and the error is not user-actionable. Observed at least twice on 2026-10-18.
Expected Behavior
A command the classifier cannot parse should degrade gracefully (e.g. "cannot classify → ask the user"), never terminate the process. A permission decision is best-effort; it must not be able to kill the session.
Actual Behavior
⚠ Error
ERROR Bad substitution: pair%%
/home/furkan/.nvm/versions/node/v22.23.2/lib/node_modules/command-code/node_modules/shell-quote/parse.js:219:12
216: varend += 1;
217: }
218: if (depth !== 0) {
219: throw new Error('Bad substitution: ' + s.slice(i));
220: }
221: varend -= 1;
222: varname = s.slice(i, varend);
- parseEnvVar (.../shell-quote/parse.js:219:12)
- (.../shell-quote/parse.js:308:17)
- at Array.map (<anonymous>)
- parseInternal (.../shell-quote/parse.js:161:17)
- parse (.../shell-quote/parse.js:361:15)
- classify (file:///.../command-code/dist/cli.mjs:2:484175)
- classifyShellCommand (file:///.../command-code/dist/cli.mjs:2:485322)
- shellPermissionRulesForCommand (file:///.../command-code/dist/cli.mjs:2:1271876)
- getShellPermissionChoices (file:///.../command-code/dist/cli.mjs:2:1280072)
- ShellPermissionPrompt (file:///.../command-code/dist/cli.mjs:16:29683)
✖ CRITICAL: Unhandled Promise Rejection!
This is an unexpected error. Please file a bug report at https://github.com/CommandCodeAI/command-code/issues/new
✖ ERROR → Error
ℹ REASON → Bad substitution: pair%%
The CLI then exits.
Steps to reproduce the issue
-
shell-quote@1.12.0(which is the latest published version, 2026-10-02) throws on an expansion it considers unterminated — the message matches the crash exactly, becauseparseEnvVardoess.slice(i)starting right after the{, and the throw is thedepth !== 0branch (scan hit end of string without a matching}):require('shell-quote').parse('echo ${pair%%') // Error: Bad substitution: pair%%
(also reproducible with
${pair##→Bad substitution: pair##, andecho ${}→Bad substitution: ${}) -
In a session, run a command containing
${var%%pattern}/${var##pattern}. The crashing one was a 1170-char loop:cd … && A="…(3)/…" && B="…(4)/…" && for pair in "a:b" "c:d"; do a="${pair%%:*}"; b="${pair##*:}"; echo "=== $a vs $b"; for ext in screen.png code.html; do ha=$(md5sum "$A/$a/$ext" 2>/dev/null|cut -c1-8); … done; done
Result: the CLI exits while the permission decision for that command is being computed.
-
Note for whoever picks this up: the full command above parses fine with shell-quote in isolation (
parse(cmd)→ 104 tokens, no throw). So the string handed toparse()inside the classifier is not the raw command — it is truncated mid-expansion (that is precisely what the message shows:s.slice(i)=pair%%…). Finding that split/truncation inclassifyShellCommand/shellPermissionRulesForCommandis the actual bug; the crash itself is the missingtry/catch.
Command Code Version
1.84.0
Operating System
Linux
- WSL2 (Ubuntu), kernel
6.18.54.1-microsoft-standard-WSL2 - node v22.23.2
Terminal/IDE
WSL2 terminal
Shell
bash (/bin/bash; note /bin/sh on this box is dash)
Fix prompt (optional)
In
dist/cli.mjs,shellPermissionRulesForCommand/classifyShellCommandcallshell-quote'sparse()on a command string to derive permission rules.parse()may throw (parseEnvVarthrowsBad substitution: …for${}and for any expansion whose closing}it cannot find), and the error escapes as an unhandled promise rejection that terminates the process. Wrap the parse call intry/catch: on failure, treat the command as unclassified and fall back to asking the user (never exit). Check first how the command string is produced beforeparse()— the raw command parses fine, so something splits/truncates it mid-expansion; that splitter should not cut inside${…}. Add a regression test that feedsecho ${pair%%and a command containing${var%%pattern}/${var##pattern}and asserts the classifier returns a "ask" decision instead of throwing.
Additional context
- Trace ID:
74f29026d9a6a223179c5175a166a0de - Sessions involved (can be exported with
/export jsonland shared privately if useful): the crashing one and the one where the error was pasted back — both in the same project; happy to provide on request. shell-quoteis already at the latest published version, so this is not a stale-dependency issue:parse()is documented to throw on input it cannot parse, and the caller must handle it.- Impact assessment: no data loss (completed writes persisted), but an irrecoverable session kill that the user cannot act on. The same command family is common in everyday work (parsing
key:valuepairs in shell loops), so this is likely to be hit again.
Activity