Why
--config means two different things depending on which command group you are in.
|
Input format |
hookdeck gateway source|destination create|upsert|update |
A JSON object string: --config '{"url":"..."}' |
hookdeck outpost destination create|update |
Repeatable key=value: --config url=... |
Same flag name, same conceptual slot, incompatible input. A user who learns one and tries the other gets an error rather than a result. --config-file is consistent across both; only --config diverges.
This was found while building the Outpost commands (#346) and is worth fixing deliberately rather than leaving as an accident of history.
What we found
Investigating why the two ended up different turned up a real asymmetry:
- Outpost destination config is entirely flat, and every value is a string. All 9 destination types, 38 field definitions, no nesting. Worth noting: the
/destination-types endpoint reports some fields as key_value_map, checkbox or select, but those are form-rendering hints, not wire types — the API accepts and stores every value as a string. Sending custom_headers as an object returns it normalised to a JSON-encoded string, byte-identical to sending it as a string.
- Gateway config nests, one level and mechanically:
auth.type, auth.token, auth.username, auth.webhook_secret_key, and so on.
- No config key in either product contains a dot.
So the two designs are each locally reasonable — flat key=value cannot express gateway's nesting, and JSON is clumsy for Outpost's flat string fields. But that is an argument for a better shared mechanism, not for keeping two.
Proposal: dotted-path key=value
Adopt --config key=value everywhere, where the key may be a dotted path:
# gateway, today
hookdeck gateway destination create --name api --type HTTP \
--config '{"auth":{"type":"BEARER_TOKEN","token":"xyz"}}'
# gateway, proposed
hookdeck gateway destination create --name api --type HTTP \
--config auth.type=BEARER_TOKEN --config auth.token=xyz
# outpost — unchanged, flat is just the trivial case
hookdeck outpost destination create --tenant-id acme --type webhook \
--config url=https://example.com/hooks
Three properties make this the right shared mechanism:
- It expresses everything either product needs, now and later. This matters most for Outpost, where destination types are defined by the deployment rather than the CLI: if a nested type ships tomorrow,
--config a.b=c expresses it with no CLI release. A flat-only scheme would leave that type uncreatable until we shipped a fix, which is exactly what not hardcoding server-owned schemas is supposed to prevent.
- It is an existing convention, not an invention. Helm's
--set a.b.c=v with --values file.yaml as the escape hatch is the same shape as --config a.b=v with --config-file.
- It costs nothing today. No config key in either product contains a dot, so there is no ambiguity to resolve up front.
What stays
--config-file keeps working in both, as the escape hatch for anything awkward inline.
- Gateway's flat flags (
--url, --bearer-token, --rate-limit) stay. They are sugar over the same config and are more discoverable wherever the CLI already knows the schema; Helm keeps both too. Only the JSON-string form of --config is affected.
Migration
--config '{...}' to --config a.b=v is breaking for anyone scripting gateway today, so:
- Gateway
--config accepts both forms during the transition — JSON when the value starts with {, dotted key=value otherwise. That standardizes the CLI in a minor release with nothing breaking.
- The JSON form is documented as deprecated and removed at the next major.
Inventory
Open question
Whether to rename the Outpost flag in the meantime. Keeping --config means carrying a known collision until gateway accepts both forms; the Outpost commands are in beta and have no users yet, so renaming is cheapest now if we would rather not carry it. Landing the gateway dual-format support instead closes the collision without a rename.
Prior art
- #346 — Outpost CLI epic, where this surfaced
- AGENTS.md §2 "OpenAPI to CLI Conversion Standards" — currently prescribes flat flags and comma-separated arrays; this adds the case it does not cover, where the schema is owned by the server rather than the CLI
- Helm
--set / --values — the closest established convention
Why
--configmeans two different things depending on which command group you are in.hookdeck gateway source|destination create|upsert|update--config '{"url":"..."}'hookdeck outpost destination create|update--config url=...Same flag name, same conceptual slot, incompatible input. A user who learns one and tries the other gets an error rather than a result.
--config-fileis consistent across both; only--configdiverges.This was found while building the Outpost commands (#346) and is worth fixing deliberately rather than leaving as an accident of history.
What we found
Investigating why the two ended up different turned up a real asymmetry:
/destination-typesendpoint reports some fields askey_value_map,checkboxorselect, but those are form-rendering hints, not wire types — the API accepts and stores every value as a string. Sendingcustom_headersas an object returns it normalised to a JSON-encoded string, byte-identical to sending it as a string.auth.type,auth.token,auth.username,auth.webhook_secret_key, and so on.So the two designs are each locally reasonable — flat key=value cannot express gateway's nesting, and JSON is clumsy for Outpost's flat string fields. But that is an argument for a better shared mechanism, not for keeping two.
Proposal: dotted-path key=value
Adopt
--config key=valueeverywhere, where the key may be a dotted path:Three properties make this the right shared mechanism:
--config a.b=cexpresses it with no CLI release. A flat-only scheme would leave that type uncreatable until we shipped a fix, which is exactly what not hardcoding server-owned schemas is supposed to prevent.--set a.b.c=vwith--values file.yamlas the escape hatch is the same shape as--config a.b=vwith--config-file.What stays
--config-filekeeps working in both, as the escape hatch for anything awkward inline.--url,--bearer-token,--rate-limit) stay. They are sugar over the same config and are more discoverable wherever the CLI already knows the schema; Helm keeps both too. Only the JSON-string form of--configis affected.Migration
--config '{...}'to--config a.b=vis breaking for anyone scripting gateway today, so:--configaccepts both forms during the transition — JSON when the value starts with{, dotted key=value otherwise. That standardizes the CLI in a minor release with nothing breaking.Inventory
--config/--credential(no behaviour change for existing flat keys)--configalongside the existing JSON forma\.b), even if nothing needs it yeta[0]=b) is needed — Outpost currently uses comma-separated strings, so this may be unnecessary--config--config key=valuewith dotted paths for nesting, plus--config-file; flat flags are sugar where the schema is CLI-knownOpen question
Whether to rename the Outpost flag in the meantime. Keeping
--configmeans carrying a known collision until gateway accepts both forms; the Outpost commands are in beta and have no users yet, so renaming is cheapest now if we would rather not carry it. Landing the gateway dual-format support instead closes the collision without a rename.Prior art
--set/--values— the closest established convention