You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Command arguments are tokenized twice on their way to a handler: once by the router, once by the handler itself. Every structural payload (JSON objects, quoted values, comma-separated lists) therefore has to survive a second de-quoting pass it was never meant to go through.
This is not a hypothetical. It has produced at least two distinct user-visible bugs, and the current fixes patch the symptom rather than the shape:
A recent --json regression — alter-cli mcp <tool> --json '{...}' rejected all payloads with Error: invalid --json: Unexpected character, taking down the entire script-facing entry point. Fixed on main, never shipped in a release.
Filing this so the class of bug is tracked, not just its instances.
The mechanism
argv → cli_execution.dart re-quotes structural args (quoteIfNeeded)
→ command_router.dart tokenizes, strips quotes, re-joins to a String
→ handler tokenizes AGAIN, strips quotes again
→ jsonDecode / validation
Step 2 is the problem: the router flattens List<String> back into a String, so anything the shell already unquoted must be re-protected — by hand, correctly, at every hop. Miss it once and the payload is silently corrupted rather than rejected.
The --json regression is exactly that miss. On the real binary:
{"chain_id":100} reached jsonDecode as {chain_id:100}. Bisected to 16b841339, which rewrote GrammarParser.tokenize to handle quotes — a correct change in isolation that broke a caller relying on the old behaviour.
Both fixes so far (#57, bbdad4a5) add a compensating re-quote at the point where breakage was observed. That works, but the next flag carrying structural data starts from the same footing.
Why it matters beyond the two known cases
The failure mode is quiet. A corrupted payload does not look like a parsing bug at the call site — it surfaces as invalid --json, or as a validation error on an address that looks fine in the shell history. For a script-based caller there is no way to tell "you passed bad JSON" from "the CLI mangled your JSON".
Suggested direction
Not prescriptive, but the options seem to be:
Keep tokens as List<String> end to end — pass the parsed argv to handlers instead of re-serializing to a String. Removes the second tokenization entirely, so no re-quoting is needed anywhere.
Single quoting authority — if the string round-trip must stay, make re-quoting a property of the router's serialization step rather than something each caller remembers, so a new flag is protected by default.
Guard the invariant with a test that reproduces the real handler contract (see below) — cheapest, but only catches known paths.
Option 1 also retires quoteIfNeeded / _requoteIfNeeded / the handlers' private _tokenize copies, which currently duplicate near-identical quote logic in three places.
Acceptance tests
Verifiable criteria for whichever direction is chosen:
alter-cli mcp chain.add --json '{"chain_id":100}' returns a decodable envelope, not invalid --json.
Nested structures and spaces survive: --json '{"a":[1,2],"b":"x y"}' reaches the handler byte-identical.
Assertions are made on the payload as received by the handler, not on router output.
Criterion 4 is the one that bites. A mock that does not re-tokenize passes against broken code — verified while writing the regression test for --json: the naive version was green with the bug present.
Regression test available
VISIALIS/phoenix_0#1002 adds the --json case, with a mock replicating McpProxyCommandHandler._tokenize. Fails without the fix (rawJson={chain_id:100}), passes with it, 29/29 in the file.
Environment
macOS 26.6.1, built from source (dart compile exe bin/cli.dart), CLI + MCP server
Released alter-cli 1.25.0 unaffected by the --json case
Summary
Command arguments are tokenized twice on their way to a handler: once by the router, once by the handler itself. Every structural payload (JSON objects, quoted values, comma-separated lists) therefore has to survive a second de-quoting pass it was never meant to go through.
This is not a hypothetical. It has produced at least two distinct user-visible bugs, and the current fixes patch the symptom rather than the shape:
lending positions-batch --users a,bprepended a stray quote to the first address, failing validation every time.--jsonregression —alter-cli mcp <tool> --json '{...}'rejected all payloads withError: invalid --json: Unexpected character, taking down the entire script-facing entry point. Fixed onmain, never shipped in a release.Filing this so the class of bug is tracked, not just its instances.
The mechanism
Step 2 is the problem: the router flattens
List<String>back into aString, so anything the shell already unquoted must be re-protected — by hand, correctly, at every hop. Miss it once and the payload is silently corrupted rather than rejected.The
--jsonregression is exactly that miss. On the real binary:{"chain_id":100}reachedjsonDecodeas{chain_id:100}. Bisected to16b841339, which rewroteGrammarParser.tokenizeto handle quotes — a correct change in isolation that broke a caller relying on the old behaviour.Both fixes so far (
#57,bbdad4a5) add a compensating re-quote at the point where breakage was observed. That works, but the next flag carrying structural data starts from the same footing.Why it matters beyond the two known cases
The failure mode is quiet. A corrupted payload does not look like a parsing bug at the call site — it surfaces as
invalid --json, or as a validation error on an address that looks fine in the shell history. For a script-based caller there is no way to tell "you passed bad JSON" from "the CLI mangled your JSON".Suggested direction
Not prescriptive, but the options seem to be:
List<String>end to end — pass the parsed argv to handlers instead of re-serializing to aString. Removes the second tokenization entirely, so no re-quoting is needed anywhere.Option 1 also retires
quoteIfNeeded/_requoteIfNeeded/ the handlers' private_tokenizecopies, which currently duplicate near-identical quote logic in three places.Acceptance tests
Verifiable criteria for whichever direction is chosen:
alter-cli mcp chain.add --json '{"chain_id":100}'returns a decodable envelope, notinvalid --json.--json '{"a":[1,2],"b":"x y"}'reaches the handler byte-identical.lending positions-batch --users 0xAAA,0xBBB— first address arrives unmodified (lending positions-batch --users a,b prepends a stray quote to the first address, always failing validation #57 stays fixed).Criterion 4 is the one that bites. A mock that does not re-tokenize passes against broken code — verified while writing the regression test for
--json: the naive version was green with the bug present.Regression test available
VISIALIS/phoenix_0#1002adds the--jsoncase, with a mock replicatingMcpProxyCommandHandler._tokenize. Fails without the fix (rawJson={chain_id:100}), passes with it, 29/29 in the file.Environment
dart compile exe bin/cli.dart), CLI + MCP serveralter-cli 1.25.0unaffected by the--jsoncase