Summary
An adapter that declares an argument named json makes every webcmd
command crash at startup with an unhandled Commander error. The official
LinkedIn plugin ships exactly such an adapter (thread-snapshot), so
installing it bricks the CLI — including plugin uninstall, so there is no
recovery path through webcmd itself.
Environment
- webcmd
0.7.6, run from source at 37036a6 (clean main, no local changes)
- Node.js v22.18.0
- Windows 11
Reproduction
Install the official LinkedIn plugin so it lands at
~/.webcmd/plugins/linkedin/ (I placed the repo's plugins/linkedin there
directly, since I was running from a source checkout — same result as
webcmd plugin install github:agentrhq/webcmd/linkedin), then run any
command:
$ webcmd --version
0.7.6 # survives: exits before command registration
$ webcmd list
node_modules\commander\lib\command.js:628
throw new Error(`Cannot add option '${option.flags}'...
^
Error: Cannot add option '--json' to command 'thread-snapshot' due to conflicting flag '--json'
- already used by option '--json [value]'
at Command._registerOption (node_modules\commander\lib\command.js:628:13)
at Command.addOption (node_modules\commander\lib\command.js:671:10)
at Command._optionEx (node_modules\commander\lib\command.js:763:17)
at Command.option (node_modules\commander\lib\command.js:789:17)
at addOutputFormatOption (src\command-surface.ts:448:6)
at configureCommandSurface (src\command-surface.ts:228:3)
at registerCommandToProgram (src\commanderAdapter.ts:54:3)
at registerAllCommands (src\commanderAdapter.ts:188:7)
at createProgram (src\cli.ts:2356:21)
at runCli (src\cli.ts:2488:19)
Every command that reaches command registration fails identically:
| command |
result |
webcmd --version |
ok |
webcmd list |
crash |
webcmd doctor |
crash |
webcmd plugin list |
crash |
webcmd plugin uninstall linkedin |
crash |
Because plugin uninstall crashes too, the only way out is deleting
~/.webcmd/plugins/linkedin by hand.
Two further problems with the failure itself: it is an unhandled exception
that prints a raw Node stack trace rather than a structured webcmd error,
and the process still exits 0, so a script driving the CLI reads the crash
as success.
Root cause
plugins/linkedin/thread-snapshot.js:162 declares an adapter argument named
json:
args: [
{ name: 'thread-url', required: true, ... },
{ name: 'max-scrolls', type: 'number', default: 30, ... },
{ name: 'json', type: 'bool', default: false, help: 'Return only JSON snapshot string in the snapshot_json field' },
],
configureCommandSurface (src/command-surface.ts:212) registers the
adapter's own arguments first, then adds the shared output-format options
unconditionally:
export function addOutputFormatOption(command, defaultFormat = 'table') {
return command
.option('-f, --format <fmt>', OUTPUT_FORMAT_HELP, defaultFormat)
.option('--json', JSON_FORMAT_ALIAS_HELP, false); // <-- no collision check
}
--json is already taken by the adapter's argument, so Commander throws. The
throw happens inside createProgram(), i.e. before any command runs, which is
why it takes down the whole CLI rather than just linkedin thread-snapshot.
The same file already knows how to do this safely — ensureOutputFormatOptions
(src/command-surface.ts:465) checks the registered flags first, and even
handles a partial -f/--format collision:
const flags = new Set<string>();
for (const option of child.options) {
if (option.short) flags.add(option.short);
if (option.long) flags.add(option.long);
}
if (!flags.has('--format')) {
child.option(flags.has('-f') ? '--format <fmt>' : '-f, --format <fmt>', OUTPUT_FORMAT_HELP, 'table');
}
if (!flags.has('--json')) child.option('--json', JSON_FORMAT_ALIAS_HELP, false);
The adapter-registration path just never got that guard.
Scope
thread-snapshot is the only adapter in the repo that currently collides, but
the same crash is reachable from any adapter argument named after a shared
option that configureCommandSurface adds — format, json, trace,
verbose, and for browser commands window, site-session, keep-tab. Any
community or private adapter declaring one of those bricks the CLI the moment
it is installed.
Expected behaviour
Installing a plugin should never be able to prevent webcmd from starting. At
minimum, a shared option whose flag an adapter already declares should be
skipped rather than registered twice, matching what ensureOutputFormatOptions
already does.
Worth deciding separately: whether an adapter should be allowed to declare an
argument that shadows a reserved flag at all, or whether webcmd validate
should reject those names up front. Happy to send a PR for the crash guard
either way.
Summary
An adapter that declares an argument named
jsonmakes everywebcmdcommand crash at startup with an unhandled Commander error. The official
LinkedIn plugin ships exactly such an adapter (
thread-snapshot), soinstalling it bricks the CLI — including
plugin uninstall, so there is norecovery path through
webcmditself.Environment
0.7.6, run from source at37036a6(cleanmain, no local changes)Reproduction
Install the official LinkedIn plugin so it lands at
~/.webcmd/plugins/linkedin/(I placed the repo'splugins/linkedintheredirectly, since I was running from a source checkout — same result as
webcmd plugin install github:agentrhq/webcmd/linkedin), then run anycommand:
Every command that reaches command registration fails identically:
webcmd --versionwebcmd listwebcmd doctorwebcmd plugin listwebcmd plugin uninstall linkedinBecause
plugin uninstallcrashes too, the only way out is deleting~/.webcmd/plugins/linkedinby hand.Two further problems with the failure itself: it is an unhandled exception
that prints a raw Node stack trace rather than a structured
webcmderror,and the process still exits
0, so a script driving the CLI reads the crashas success.
Root cause
plugins/linkedin/thread-snapshot.js:162declares an adapter argument namedjson:configureCommandSurface(src/command-surface.ts:212) registers theadapter's own arguments first, then adds the shared output-format options
unconditionally:
--jsonis already taken by the adapter's argument, so Commander throws. Thethrow happens inside
createProgram(), i.e. before any command runs, which iswhy it takes down the whole CLI rather than just
linkedin thread-snapshot.The same file already knows how to do this safely —
ensureOutputFormatOptions(
src/command-surface.ts:465) checks the registered flags first, and evenhandles a partial
-f/--formatcollision:The adapter-registration path just never got that guard.
Scope
thread-snapshotis the only adapter in the repo that currently collides, butthe same crash is reachable from any adapter argument named after a shared
option that
configureCommandSurfaceadds —format,json,trace,verbose, and for browser commandswindow,site-session,keep-tab. Anycommunity or private adapter declaring one of those bricks the CLI the moment
it is installed.
Expected behaviour
Installing a plugin should never be able to prevent
webcmdfrom starting. Atminimum, a shared option whose flag an adapter already declares should be
skipped rather than registered twice, matching what
ensureOutputFormatOptionsalready does.
Worth deciding separately: whether an adapter should be allowed to declare an
argument that shadows a reserved flag at all, or whether
webcmd validateshould reject those names up front. Happy to send a PR for the crash guard
either way.