Skip to content

hookdeck ci --local also rewrites the global config and switches the active project #332

Description

@leggetter

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions