Skip to content

Fix mis-modelled multi-arg options: ecosystem CLIs and macOS tools - #345

Merged
acarl005 merged 2 commits into
mainfrom
spec-audit/ecosystem-and-macos-multi-arg-options
Aug 24, 2026
Merged

Fix mis-modelled multi-arg options: ecosystem CLIs and macOS tools#345
acarl005 merged 2 commits into
mainfrom
spec-audit/ecosystem-and-macos-multi-arg-options

Conversation

@warp-agent-staging

@warp-agent-staging warp-agent-staging Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Audit of option args arity across the ecosystem CLIs and macOS-only tools group, following the ln fix in #341. An option's args describe values the option itself consumes; where our specs put a command's operands, an alternative spelling of one value, or a value set into that array, the model is wrong and completion can misbehave, since the completer resolves the last declared arg regardless of which value is being typed. How visible that is varies per option — several of these corrections are arity-only, with no change a user can see today, and each says so.

Every option below was checked against the tool's own ground truth — --help, the man page, or the tool's source — before any edit. sips and pmset are macOS-only and this work was done on Linux, so they were verified against Apple's published man pages rather than by running them; that is called out per option.

Where an option turned out to be a boolean flag whose operands belong to the command, those operands are simply removed and left unmodelled. They are not relocated onto the command or subcommand unless they are a genuine positional in their own right, because operands that are valid only in combination with a particular flag cannot be expressed by the schema, and declaring them unconditionally would advertise invocations that are not valid. Both such cases below say plainly what completion data is lost.

Tool versions used: Elixir 1.14.0, Clojure CLI 1.12.5.1664, conda 26.5.3, hugo v0.123.7, stripe 1.50.4, vault 1.18.3, LastPass CLI 1.3.7.

Changed

Category 3 — a value set enumerated as args

lpass share useradd --read-only, --hidden, --admin and the same three under lpass share usermod (6 options, lpass.json)

Each declared ["true", "false"] — two arguments. Ground truth, from the CLI's own usage output:

lpass share useradd [--read-only=[true|false] --hidden=[true|false] --admin=[true|false] SHARE USERNAME

and from cmd-share.c in lastpass/lastpass-cli, where all three are declared with required_argument:

{"read-only", required_argument, NULL, 'r'},
{"hidden",    required_argument, NULL, 'H'},
{"admin",     required_argument, NULL, 'a'},

One required value each, whose permitted values are true and false. Each is now a single true|false argument carrying both values as suggestions; the per-value descriptions are preserved. SHARE and USERNAME were already correctly modelled as the subcommands' own positionals, and are untouched. requiresSeparator was deliberately not added: required_argument under getopt_long accepts both --read-only=true and --read-only true.

Category 2 — one value modelled as two

vault operator generate-root -pgp-key (vault.json)

Declared [string (filepaths), keybase:user]. From vault operator generate-root -help:

  -pgp-key=<keybase:user>
      Path to a file on disk containing a binary or base64-encoded public PGP
      key. This can also be specified as a Keybase username using the format
      "keybase:<username>". [...]

One value; keybase:<username> is an alternative form of that value, not a second value. Collapsed to a single pgp_key argument with template: filepaths plus a keybase:user1 suggestion — the same shape the identical vault operator init -root-token-pgp-key option already uses in this spec.

Caveat from the same GUI pass: this option shows nothing in either build, and the untouched -format control — four declared suggestions — behaves identically, so multi-character single-dash options may get no value completion at all. Cause unconfirmed and not chased here. The correction stands on the documented arity; it is simply not visible today.

Category 1 — operands written into the option

hugo new -k/--kind (hugo.json)

Declared [archetype|default, content-section/file-name.md]. From hugo new --help:

Usage:
  hugo new content [path] [flags]

Flags:
  -k, --kind string         content type to create

--kind takes one string. The second argument was a verbatim copy of hugo new's own positional, which the subcommand already declares. Removed.

This is an arity correction with no observable change in completion today. GUI validation on both builds, with a populated ./archetypes/, produced byte-for-byte identical menus before and after, and hugo new --kind gal offers nothing in either. The archetype generator above is inert: it is written Fig-style as "generators": {"script": ..., "postProcess": ...}, and Arg in completion-metadata/src/fig_types.rs has no field for an inline generators object — only generatorName, which points at a Rust generator. There is no generatorName anywhere in hugo.json and no hugo module in command-signatures/src/generators/, and since the struct does not deny unknown fields, serde drops the key silently. So the removed argument was not hiding anything.

That parser gap is worth naming, because it is the actual reason hugo new -k does not complete archetypes and the spec looks like it should: inline generators appears 962 times across 142 files in command-signatures/json/, none of it read. Fixing hugo's archetype completion means porting that script to a Rust generator and referencing it by generatorName, not editing this argument.

clojure -m/--main (clojure.json)

Declared [ns-name, args (variadic)]. From clojure -h:

  Run main      clojure [clj-opt*] -M[aliases] [init-opt*] [main-opt] [arg*]

main-opt:
 -m, --main ns-name  Call the -main function from namespace w/args

-m takes ns-name; arg* is a trailing operand of the command line, not a value of the option — the "w/args" wording in the description is what the second arg was inferred from. Removed, leaving ns-name (with its isScript) resolvable.

stripe config --set (stripe.json)

Declared [option, value]. --set is a boolean flag, and the pair are positionals of stripe config that are only accepted alongside it. Three independent confirmations:

  • stripe config --help lists --set with no value type, in contrast to its neighbours --unset string and --remove-profile string; the example given is stripe config --set color off.
  • pkg/cmd/config.go in stripe/stripe-cli: cmd.Flags().BoolVar(&cc.set, "set", false, "Set a config field to some value"), dispatched by case cc.set && len(args) == 2:; every other case prints help.
  • Observed: stripe config --set color prints usage and does nothing; stripe config --set color off writes color = 'off'.

--set is now a boolean flag with no arguments. The key and value are left unmodelled, so there is no longer any completion hint for them. They could not be moved to stripe config's own positionals: stripe config option value without --set is not a valid invocation, so declaring them there would advertise a command that does not exist. The schema cannot express "these operands only after this flag", so the honest outcome is that they go unmodelled.

pmset -a, -b, -c, -u (pmset.json)

Each declared [setting, value], with the same 19-entry setting list copied four times. macOS-only; verified against Apple's own pmset.1 from the PowerManagement project in the apple-oss-distributions GitHub organisation, since I could not run pmset on Linux. SYNOPSIS:

pmset [-a | -b | -c | -u] [setting value] [...]

and the SETTING section:

You may specify one or more setting & value pairs on the command-line invocation of pmset. The -a, -b, -c, -u flags determine whether the settings apply to battery (-b), charger (wall power) (-c), UPS (-u) or all (-a).

The four flags select a scope and consume no value; the setting/value pairs are pmset's operands and there may be several of them. All four are now boolean flags, and pmset's existing root argument (the sleepnow/noidle/lock/touch overrides) is untouched.

This drops the 19-entry settings list from the spec, and that is a real loss worth flagging. displaysleep, disksleep, hibernatemode and the rest were described there, wrongly attached to the flag and with the nameless value arg declared after them; whether they ever reached a menu is not something I measured, and after the hugo case below I will not assert it either way. The data was in the spec and now is not. It could not be moved to pmset's root arguments: a setting/value pair is only meaningful after a scope flag, so declaring it at the root would suggest pmset displaysleep 10, which is invalid, and would change completion for invocations that have nothing to do with these four flags. The schema has no way to say "these operands only after this flag". If conditional operands are worth supporting, this is a concrete case for it — the settings list and their descriptions are recoverable from this PR's diff.

Verified correct, left alone

sips — macOS-only; verified against the sips(1) man page as shipped with macOS (mirror: keith.github.io/xcode-man-pages/sips.1.html), cross-checked against ss64.com/mac/sips.html. Both copies agree that all eight genuinely take two values, exactly as the spec declares:

  • -s/--setProperty key value — "Set a property value for key to value."
  • -X/--extractTag tag tagFile — "Write a profile tag element to tagFile."
  • --copyTag srcTag dstTag — "Copy the srcTag element of a profile to dstTag."
  • --loadTag tag tagFile — "Set the tag element of a profile to the contents of tagFile."
  • -M/--matchToWithIntent profile intent
  • -c/--cropToHeightWidth pixelsH pixelsW
  • -p/--padToHeightWidth pixelsH pixelsW
  • -z/--resampleHeightWidth pixelsH pixelsW

conda config — from conda config --help (conda 26.5.3), each is nargs=2:

  --append KEY VALUE    Add one configuration value to the end of a list key.
  --prepend, --add KEY VALUE
                        Add one configuration value to the beginning of a list key.
  --set KEY VALUE       Set a boolean or string key.
  --remove KEY VALUE    Remove a configuration value from a list key. [...]

elixir and iex — from elixir --help (Elixir 1.14.0):

  --rpc-eval NODE "COMMAND"    Evaluates the given command on the given remote node (*)
  --boot-var VAR "VALUE"       Makes $VAR available as VALUE to FILE.boot (*)
  --pipe-to "PIPEDIR" "LOGDIR" Starts the Erlang VM as a named PIPEDIR and LOGDIR

Two values each. iex --help states "It accepts all other options listed by elixir --help", so the three identical options on iex are settled by the same lines.

clojure -X and -T — from clojure -h (CLI 1.12.5.1664):

  Exec fn(s)    clojure [clj-opt*] -X[aliases] a/fn? [kpath v]* kv-map?
  Run tool      clojure [clj-opt*] -T[name|aliases] a/fn [kpath v] kv-map?

The three declared args (a/fn, variadic kvs, kv-map) match the documented shape in order, including the kv-map? trailing map added in the 1.12 CLI. Genuinely multi-valued; unchanged.

Unresolved

None. Every option in this group resolved against ground truth.

Noticed but out of scope (not touched)

  • sips -i/--addIcon declares an icon filepaths argument, but the man page shows it taking none: "-i / --addIcon Add a Finder icon to image file." A single spurious arg rather than the ≥2 class, and outside the assigned list.
  • conda.json uses "name": "--prepend, --add" as one literal option name (and "-c, --channel" elsewhere) instead of a name array, so the aliases are unlikely to match.
  • clojure -T declares a/fn as isOptional, but clj -h shows it required for -T (optional only for -X).
  • clojure models no command-level positional for the path main-opt or the trailing arg*.
  • Fig-style inline "generators" objects are not read by this crate at all — 962 occurrences across 142 files in command-signatures/json/, silently dropped by serde because Arg only has generatorName. hugo new -k is one user-visible instance; see that section above.

Testing

cargo test -p warp-command-signatures176 passed, 0 failed, 0 ignored. All six edited specs are clean under npm run format.

Audit of option `args` arity across the ecosystem CLIs and macOS-only
tools, following the `ln` fix in #341. Every option was checked against
the tool's own help output, man page, or source before editing.

- lpass share useradd/usermod --read-only/--hidden/--admin: each takes
  one value that is either true or false, not two values.
- hugo new -k/--kind: takes one string; the second declared arg was a
  copy of `hugo new`'s own path positional.
- clojure -m/--main: takes ns-name only; `arg*` is a trailing operand.
- stripe config --set: boolean flag; the key and value are positionals
  of `stripe config`.
- pmset -a/-b/-c/-u: scope selectors that take no value; the setting and
  value operands now live on pmset itself.
- vault operator generate-root -pgp-key: one value with two forms (file
  path or keybase:<username>), modelled like the sibling pgp key options.

sips, conda config, elixir/iex and clojure -X/-T were verified as
genuinely multi-valued and left unchanged.
@warp-agent-staging

Copy link
Copy Markdown
Contributor Author

This PR was generated with Warp.

Comment @warp-factory on this PR to send it follow-up work.

View run View conversation View on Slack

Review feedback: the operands removed from `stripe config --set` and
`pmset -a/-b/-c/-u` are only valid in combination with those flags, so
moving them onto the command and subcommand advertised invocations that
are not valid (`stripe config option value`, `pmset displaysleep 10`).

The schema cannot express operands conditional on a flag, so they are
left unmodelled instead. The flags stay boolean, which was the correct
part. pmset's existing root argument is restored untouched.

This loses the 19-entry pmset settings list from completion; the PR body
says so explicitly rather than dropping it quietly.

@warp-agent-staging warp-agent-staging Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-hunk reasoning for this audit, inline on the diff, so each change can be checked against the evidence that settles it without re-deriving it. The PR body carries the same evidence organised by tool, plus the options that were verified and left alone.

Two of those deserve a pointer here, since they have no hunk to anchor to. sips looks exactly like the shapes being corrected in this diff — -s key value, -X tag tagFile, --copyTag srcTag dstTag, --loadTag tag tagFile, -M profile intent, and -c/-p/-z pixelsH pixelsW — but the man page gives each of the eight two values, so all eight are correct as they stand and sips.json is untouched. Same for conda config --append/--prepend,--add/--set/--remove (KEY VALUE, argparse nargs=2), the three elixir/iex options, and clojure -X/-T, whose three declared args match a/fn? [kpath v]* kv-map? in the CLI's own usage.

Responding as wilson: Open session · View in factory

"description": "Grant write access"
}
]
"args": {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lpass share prints its own usage as useradd [--read-only=[true|false] --hidden=[true|false] --admin=[true|false] SHARE USERNAME, and cmd-share.c declares all three long options with required_argument. Each therefore takes one value drawn from a two-value set, rather than two values. SHARE and USERNAME were already the subcommand's own positionals and are untouched.

"description": "User can view entry and secrets"
}
]
"args": {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same shape as --read-only above, settled by the same usage line and the same required_argument declaration.

"description": "User cannot administer entry"
}
]
"args": {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same shape as --read-only above.

"description": "Grant write access"
}
]
"args": {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usermod carries the identical three flags in its usage line and shares the option table in cmd-share.c; see the useradd --read-only comment.

"description": "User can view entry and secrets"
}
]
"args": {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

"description": "Value for config option"
}
]
"description": "Set a value for the specified configuration option"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--set is boolean: pkg/cmd/config.go declares it BoolVar(&cc.set, "set", false, ...) and reaches WriteConfigField(args[0], args[1]) only under case cc.set && len(args) == 2:, every other case printing help. The key and value are positionals of stripe config accepted only alongside --set, and stripe config option value on its own is not a valid command, so declaring them on the subcommand would advertise one that does not exist.

The schema cannot express operands conditional on a flag, so they are left unmodelled: there is now no completion hint for the key or the value.

"name": "value"
}
]
"description": "Settings for all"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apple's pmset.1 synopsis is pmset [-a | -b | -c | -u] [setting value] [...], and its SETTING section says the flags "determine whether the settings apply to battery (-b), charger (wall power) (-c), UPS (-u) or all (-a)" — scope selectors that consume no value of their own. The setting/value pair is pmset's operand, and it had been written into each of the four flags, so this 19-entry settings list existed in four copies.

The pair now goes unmodelled rather than moving to pmset's root argument: a pair is only meaningful after a scope flag, and at the root it would suggest pmset displaysleep 10, which is not valid. That has a real cost — pmset -a <tab> no longer offers displaysleep, hibernatemode and the rest. The list and its descriptions are recoverable from this diff if operands conditional on a flag ever become expressible.

macOS-only, so verified against pmset.1 in apple-oss-distributions/PowerManagement rather than by running the tool.

"name": "value"
}
]
"description": "Settings for battery"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second of the four scope selectors, and the second copy of the settings list; see the -a comment.

"name": "value"
}
]
"description": "Settings for charger"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as -a above.

"name": "value"
}
]
"description": "Settings for UPS"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as -a above.

@acarl005
acarl005 marked this pull request as ready for review August 23, 2026 22:33
@warp-for-oss

warp-for-oss Bot commented Aug 23, 2026

Copy link
Copy Markdown

@acarl005

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@warp-for-oss warp-for-oss Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR corrects several command-signature option arities by collapsing single-value options, removing operands that were incorrectly modeled as option arguments, and preserving value suggestions where the option genuinely accepts one constrained value.

Concerns

  • No blocking concerns found. The diff adds no code comments or tests to audit, and spec_context.md states that no approved or repository spec context was found. The changed JSON metadata does not introduce security-sensitive behavior.

Verdict

Found: 0 critical, 0 important, 0 suggestions

Approve

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@acarl005
acarl005 merged commit 88e8a28 into main Aug 24, 2026
8 checks passed
@acarl005
acarl005 deleted the spec-audit/ecosystem-and-macos-multi-arg-options branch August 24, 2026 02:14
@acarl005 acarl005 self-assigned this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant