Summary
hookdeck ci --api-key <key> --local writes the local .hookdeck/config.toml as documented, but also rewrites the global ~/.config/hookdeck/config.toml, switching the CLI's active project.
The flag's help text is Save credentials to current directory (.hookdeck/config.toml), which reads as "instead of globally". In practice it is "in addition to globally".
Version
hookdeck version 2.4.0 (Homebrew, macOS arm64).
Reproduce
Starting global config:
profile = 'default'
[default]
api_key = <redacted>
guest_url = <redacted>
project_id = 'tm_AAAAAAAAAAAA' # project A
project_mode = 'inbound'
project_type = 'Gateway'
Then, in an empty directory, with an API key for a different project B:
$ hookdeck ci --api-key $PROJECT_B_KEY --local
The Hookdeck CLI is configured on project <B> in organization <Org>
Local config is created as expected:
$ ls .hookdeck/
config.toml
But the global config has also changed:
[default]
api_key = <redacted>
guest_url = <redacted>
-project_id = 'tm_AAAAAAAAAAAA'
+project_id = 'tm_BBBBBBBBBBBB'
project_mode = 'inbound'
project_type = 'Gateway'
The active project for every other hookdeck invocation on the machine has silently moved from A to B.
Cause
In pkg/cmd/ci.go, runCICmd calls CILogin unconditionally and only then considers --local:
if err := login.CILogin(&Config, lc.apiKey, lc.name); err != nil {
return err
}
if lc.local {
return saveLocalConfig()
}
CILogin (pkg/login/client_login.go) writes the global profile as part of its normal flow:
config.Profile.ApplyCIClient(response)
if err = config.Profile.SaveProfile(); err != nil {
return err
}
So --local does not redirect the write, it appends a second one. The global write has already happened by the time the flag is inspected.
Supporting evidence that this is unintended: pkg/config/config.go already has a UseProjectLocal alongside UseProject, i.e. the local/global distinction exists in the config layer — the ci path just does not use it for the profile save.
Impact
Anyone using the CLI for other work finds their environment repointed by a command whose whole purpose is to avoid touching shared state. It is silent: nothing in the output mentions the global config.
We hit this building a Hermes Agent plugin that shells out to hookdeck listen. We reached for --local specifically to avoid mutating the developer's session, and it mutated it anyway. We had checksummed the file beforehand and restored it, but that is not something a normal user would do.
Suggested fix
Skip the global profile save when --local is set — e.g. pass the flag down into CILogin and have it call UseProjectLocal/write to the local path instead of SaveProfile(). Failing that, at minimum make the flag's help and output state that the global active project is also changed.
Workaround
--hookdeck-config <path> behaves correctly: it writes only the given file and leaves the global config untouched (verified on 2.4.0, both for ci and listen). Note the two flags are mutually exclusive, which is reasonable — but it does mean --hookdeck-config is currently the only way to authenticate a CLI session without side effects.
Summary
hookdeck ci --api-key <key> --localwrites the local.hookdeck/config.tomlas documented, but also rewrites the global~/.config/hookdeck/config.toml, switching the CLI's active project.The flag's help text is
Save credentials to current directory (.hookdeck/config.toml), which reads as "instead of globally". In practice it is "in addition to globally".Version
hookdeck version 2.4.0(Homebrew, macOS arm64).Reproduce
Starting global config:
Then, in an empty directory, with an API key for a different project B:
Local config is created as expected:
But the global config has also changed:
The active project for every other
hookdeckinvocation on the machine has silently moved from A to B.Cause
In
pkg/cmd/ci.go,runCICmdcallsCILoginunconditionally and only then considers--local:CILogin(pkg/login/client_login.go) writes the global profile as part of its normal flow:So
--localdoes not redirect the write, it appends a second one. The global write has already happened by the time the flag is inspected.Supporting evidence that this is unintended:
pkg/config/config.goalready has aUseProjectLocalalongsideUseProject, i.e. the local/global distinction exists in the config layer — thecipath just does not use it for the profile save.Impact
Anyone using the CLI for other work finds their environment repointed by a command whose whole purpose is to avoid touching shared state. It is silent: nothing in the output mentions the global config.
We hit this building a Hermes Agent plugin that shells out to
hookdeck listen. We reached for--localspecifically to avoid mutating the developer's session, and it mutated it anyway. We had checksummed the file beforehand and restored it, but that is not something a normal user would do.Suggested fix
Skip the global profile save when
--localis set — e.g. pass the flag down intoCILoginand have it callUseProjectLocal/write to the local path instead ofSaveProfile(). Failing that, at minimum make the flag's help and output state that the global active project is also changed.Workaround
--hookdeck-config <path>behaves correctly: it writes only the given file and leaves the global config untouched (verified on 2.4.0, both forciandlisten). Note the two flags are mutually exclusive, which is reasonable — but it does mean--hookdeck-configis currently the only way to authenticate a CLI session without side effects.