Compatibility
Published
Everything else on this site that breaks under member renaming breaks because a name crosses a boundary between your build and some other system. A command-line flag crosses a stranger boundary than most: the option object's keys do not exist anywhere in your source at all. They are manufactured at runtime, character by character, out of the text a human being typed into a terminal. Then your code reads them back with dot access, and dot access is what renaming rewrites.
What was measured
One sample: a small argument parser of the shape every CLI has, plus the code that consumes its output. The parser walks the arguments, splits long options on the equals sign, converts kebab-case to camelCase, and stores each value under the resulting name. The consumer reads four options directly, merges them over a defaults literal, branches on the safety flag, and runs an unknown-flag check against a list of known names held as strings.
The arguments themselves are supplied by the test runner from outside the file, exactly as a shell supplies them. The sample never writes outFile, workers, dryRun or profile as literal property names on the options object; those keys come into existence only when the parser transforms the flag text. That is what makes the arms below mean anything -- a CLI sample that hardcoded its own options would have had both halves renamed together and reported a clean sweep.
The base column is clean on all five profiles: default target, modern target, both gate profiles and the string transform all reproduced the original output character for character, including the parsed values and the positional arguments. Protection on its own does not affect argument parsing.
The tool ignores every flag you typed
With member renaming on and a pattern matching three of the option names, all three direct reads returned undefined, and the merged options collapsed to defaults: effective=dist/app.js|4|true|release became effective=out.js|1|true|default.
The user asked for four workers, a release profile and a specific output path. The tool used one worker, the default profile and a different file, reported nothing unusual, and exited successfully. Isolating a single option shows the same thing at the smallest scale: with only the output-path key renamed, the run printed action=WOULD-WRITE out.js instead of dist/app.js -- a build artifact written to the wrong path, by a tool that was told the right one.
The mechanism is the one from the configuration article, arriving from a different direction. The parser stores under the name the flag text spells, because that name is computed from a string. The consumer reads under the renamed name. The defaults literal in the source is renamed too, so the merge loop asks for generated names, finds nothing, and supplies a default for every option. Everything is internally consistent; it is simply consistent with the wrong half of the contract.
The flag that stops the tool from doing damage
One arm deserves to be read on its own. With a pattern matching only the dry-run flag, the sample went from action=WOULD-WRITE dist/app.js to action=WRITING dist/app.js.
The user passed --dry-run. The parser recorded it. The protected build could not see it, the merge supplied the default of false, and the tool took the destructive branch. No error, no warning, exit code zero.
This is the same shape as a result measured on this site for a subprocess output cap, where renaming did not change a limit but removed it -- and it generalises into a rule worth keeping. Safety flags share a property that makes them uniquely exposed to this: their default is the unsafe value, because the safe behaviour is the one you opt into. A renamed read of an ordinary option gives you a wrong-but-harmless default. A renamed read of --dry-run, --no-publish or --confirm gives you the behaviour the flag exists to prevent. Nobody passes those flags by accident, and nobody has a test for the path where they are silently dropped.
The control arm that found something anyway
One arm was included purely as a control: a key the sample both writes and reads itself, which should be renamed on both sides and behave identically. It did behave identically where the value was concerned -- and it still changed the output, in a way worth reporting.
The unknown-flag check went from unknown=none to unknown=_0x1. The check enumerates the keys actually present on the options object and compares them against a list of known names held as strings. The internal key was renamed; the string list was not; so the validator sees a key it does not recognise and reports it as an unknown flag.
In this sample that is one stray line of output. In a real CLI it is a hard failure, because the standard behaviour on encountering an unknown flag is to print usage and exit non-zero. A tool built that way would reject every invocation, including one with no flags at all, and the flag it names in the error message is a generated identifier the user has never seen and cannot remove. That is the one arm in this pass where renaming turns a working program into one that refuses to start -- which, given the alternative, is the outcome you would choose.
What this means in practice
Protection does not break command-line tools. Five profiles, identical output, including the parser and everything downstream of it.
With member renaming on, a CLI has an unusually clean rule available to it, because its exposed names are enumerable: they are exactly the flags in your --help output, in camelCase. Put that list in your reserved names and the whole surface is covered. This is one of the few cases where you can be sure you have found every name, and it is worth doing explicitly rather than relying on an anchored pattern, since option names like profile, workers, force and output are exactly the short generic identifiers a broad pattern sweeps up.
The verification is quick and belongs in whatever smoke test you already run on the packaged tool. Invoke the protected binary once with every flag set to a non-default value and print the effective options. Every value should be the one you passed. Then invoke it with the safety flag and confirm the tool reports the non-destructive path -- that single assertion covers the one failure in this pass that costs you data rather than time.
Frequently asked questions
Does obfuscation break command-line argument parsing?
Not in the default configuration. A sample containing a kebab-to-camel argument parser, direct option reads, a defaults merge, a safety-flag branch and an unknown-flag check produced identical output on all five protection profiles measured. Flags only become a surface when member renaming is switched on.
Why does my CLI ignore the flags I pass after protection?
Because the option keys are built at runtime from the flag text and your reads were renamed. The parser stores under the name the user typed; the consumer asks for the generated name; the defaults literal was renamed too, so the merge finds nothing and supplies a default for every option. In the measured run four explicitly passed options all reverted to defaults with no error.
Can member renaming disable a --dry-run flag?
Yes, and this was measured directly. With only the dry-run key renamed, the sample went from WOULD-WRITE to WRITING: the flag was passed, the parser recorded it, the protected build could not see it, and the destructive branch ran with exit code zero. Safety flags are the worst case here because their default is the unsafe value.
Why does my tool report an unknown flag that nobody passed?
Because an internal key was renamed while the list of known names, held as strings, was not. The check enumerates the keys present on the options object, sees a generated identifier, and reports it as unknown. A CLI that exits non-zero on unknown flags will then refuse every invocation, naming a flag the user has never seen.
Does this affect yargs, commander and similar parsers?
The mechanism does not depend on which parser you use, because every one of them builds option keys out of the flag text at runtime. What matters is whether your rename pattern matches the camelCase names your code reads off the resulting options object.
Which names should a CLI put in its reserved list?
Exactly the flags in your --help output, converted to the camelCase form your code reads. A CLI is one of the few cases where the externally owned names are fully enumerable, so the list can be complete rather than approximate. It is worth doing explicitly, because option names tend to be short generic words that a broad pattern sweeps up.
How do I verify flags survived protection?
Invoke the packaged tool once with every flag set to a non-default value and print the effective options; each should be the value you passed. Then invoke it with the safety flag and assert that it reports the non-destructive path. That second assertion covers the only failure in this area that costs data rather than time.
Related reading