feat(listen): expose --cli-key, adopt its project, and save it when unauthenticated - #329
Merged
Conversation
The Hookdeck Console hands users a ready-to-run command: npx hookdeck-cli listen [port] <source> --cli-key <key> Two things made that worse than it needed to be. The flag was invisible. --cli-key is declared on the root command as "(deprecated)" and MarkHidden, so it appears in no help output, even though listen's own help documents it with an example. Anything that introspects the CLI concludes the flag does not exist. Declaring it on listen, bound to the same Config field, changes no behaviour and makes it discoverable in `hookdeck listen --help`. The key was also forgotten immediately. It applied to one invocation, so every later run needed it pasted again. listen now validates and saves the key when the machine has no stored credential, after which the flag can be dropped. It deliberately does not save when a credential already exists. Forwarding a Console source for a few minutes should not silently replace an existing login, so in that case the key applies to the run only. Validation happens before the write, so a typo fails with a clear error instead of being persisted. Config.HasStoredAPIKey records whether a key was on disk before InitConfig folds in the flag value, which the coalesced Profile.APIKey can no longer distinguish. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t
A key given on the command line was sent alongside the project id read from the config file. Those belong to different logins, so anyone with an existing profile who ran the command the Hookdeck Console gives them got "your API key is invalid or expired" and no way to see why. Validation is already project-agnostic (Client.clientForCLIAuthValidate sends an empty ProjectID), so it resolves the project the key really belongs to. Adopting that response replaces the stale project for the run. Saving stays conditional on there being no stored credential, so an existing login is still never replaced. Renames the helper to applyCliKey, since it now always resolves context and only sometimes saves. Tests cover the four branches without touching the network, using the httptest pattern from gateway_test.go: the flag-absent no-op (pointed at a dead port, so a stray validate call fails loudly), adopting the key's project over a stale one, leaving an existing login on disk, and refusing to write a key that fails validation. A mutation check confirms the project-adoption test fails when the fix is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t
Contributor
There was a problem hiding this comment.
Pull request overview
This PR improves the hookdeck listen “Console copy/paste” flow by making --cli-key discoverable on the listen command, ensuring the key’s project context is adopted (avoiding stale project_id from prior logins), and persisting the key only when the machine has no stored credentials yet.
Changes:
- Add a visible
--cli-keyflag tohookdeck listenwhile keeping the root flag hidden/deprecated. - Resolve and adopt the project context returned by
/cli-auth/validatewhen--cli-keyis supplied, preventing mismatched key/project headers. - Track whether an API key existed on disk pre-coalesce (
HasStoredAPIKey) and persist--cli-keyonly for first-time (unauthenticated) setups, with unit tests for both behaviors.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| pkg/config/config.go | Adds HasStoredAPIKey to distinguish stored credentials from flag-supplied keys during InitConfig coalescing. |
| pkg/config/has_stored_api_key_test.go | Unit tests validating HasStoredAPIKey behavior across stored vs flag-supplied key scenarios. |
| pkg/cmd/listen.go | Declares --cli-key on listen and adds applyCliKey to validate/adopt project context and optionally persist credentials. |
| pkg/cmd/listen_cli_key_test.go | Unit tests covering project adoption, persistence guard behavior, and flag visibility on listen. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
`--cli-key=` satisfies cobra's Changed check but carries nothing to authenticate with. The empty value then fell through InitConfig's coalesce and the run failed with "your API key is invalid or expired", which describes neither what happened nor how to fix it, after a pointless round-trip to the API. Fail immediately with an error naming the flag. Reads the flag value rather than Profile.APIKey, which by that point may hold a stored key from the config file instead of what the user typed. Raised by Copilot review on #329. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Hookdeck Console hands people a ready-to-run command:
Three things made that worse than it needed to be. New Console docs (hookdeck/website#727) document this flow, which is how they surfaced.
1. The flag was invisible
--cli-keyis declared on the root command as"(deprecated)"andMarkHidden, so it appears in no help output — nothookdeck --help, nothookdeck listen --help— even thoughlisten's own long help documents it with a worked example, and the Console API'scmd_hinttells users to run exactly that.Anything introspecting the CLI concludes the flag doesn't exist. The website's docs verifier does precisely that and rejected the documented command:
Now declared on
listen, bound to the sameConfigfield. No behaviour change; it just becomes discoverable. The root flag stays hidden and deprecated.2. A supplied key was used with the wrong project
apiclient.gosetsapiClient.ProjectID = c.Profile.ProjectId. AfterInitConfig,APIKeyholds the flag's value whileProjectIdstill holds the project from a previous login. They belong to different projects, so every call failed:Anyone who had ever logged in and then pasted a Console command hit this, with nothing pointing at the cause. Reproducible on unpatched
main.Client.clientForCLIAuthValidate()already sendsProjectID: "", so validation is project-agnostic and its response carries the key's real project. Adopting that replaces the stale one for the run.3. The key was forgotten immediately
It applied to one invocation, so every later run needed it pasted again.
listennow saves it when the machine has no stored credential, after which the flag can be dropped.It deliberately does not save when a credential already exists — forwarding a Console source for a few minutes shouldn't silently replace someone's login. In that case the key applies to the run only. Validation happens before any write, so a typo fails with a clear error rather than being persisted.
Config.HasStoredAPIKeyrecords whether a key was on disk beforeInitConfigfolds in the flag value, which the coalescedProfile.APIKeycan no longer distinguish.Worth flagging
This introduces an asymmetry.
--api-keypassed as a flag does not persist (verified:listen --api-key,gateway ... --api-keywith both valid and invalid keys). After this,listen --cli-keydoes. That reads as intentional to me —--cli-keymeans "this is my identity, set me up",--api-keymeans "use this for this call" — and it reinforces the user-scoped vs project-scoped distinctionlisten's help already draws. But it is a divergence between two flags currently bound to the same variable, and worth a second opinion.Overlaps with
feat/cli-guest-tracking-fixes. That branch removes the root--cli-keyentirely and declares one onloginonly, which would breaklisten --cli-key— the command the Console tells people to run. ItsConfigureFromClaimedCliKeyis close to what this adds and the two should converge; this is the smaller change that unblocks the docs now.Test plan
go build ./...,go vet ./...(one pre-existing warning inpkg/login, present onmain)go test ./...— 14 packages passhttptestpattern fromgateway_test.go, no networkverify-code-examplesgoes green — its verifier runs the published binary🤖 Generated with Claude Code
https://claude.ai/code/session_01HHJQ1QSdKmJMivqw7SER6t