Skip to content

[Bug] Shell-permission classifier kills the CLI: unhandled shell-quote "Bad substitution" error (1.84.0) #1043

Open

Description

@FurkanDereci

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

  1. 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, because parseEnvVar does s.slice(i) starting right after the {, and the throw is the depth !== 0 branch (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##, and echo ${} → Bad substitution: ${})

  2. 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.

  3. 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 to parse() 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 in classifyShellCommand / shellPermissionRulesForCommand is the actual bug; the crash itself is the missing try/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 / classifyShellCommand call shell-quote's parse() on a command string to derive permission rules. parse() may throw (parseEnvVar throws Bad 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 in try/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 before parse() — the raw command parses fine, so something splits/truncates it mid-expansion; that splitter should not cut inside ${…}. Add a regression test that feeds echo ${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 jsonl and 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-quote is 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:value pairs in shell loops), so this is likely to be hit again.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions