Skip to content

Tags: hookdeck/hookdeck-cli

Tags

v3.1.0

Toggle v3.1.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #465 from hookdeck/dependabot/go_modules/github.co…

…m/tidwall/pretty-1.2.2

chore(deps): Bump github.com/tidwall/pretty from 1.2.1 to 1.2.2

v3.0.3

Toggle v3.0.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #459 from hookdeck/dependabot/go_modules/github.co…

…m/creack/pty-1.1.24

chore(deps): Bump github.com/creack/pty from 1.1.17 to 1.1.24

v3.0.2

Toggle v3.0.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #451 from hookdeck/docs/acceptance-concurrency-can…

…cels

docs: acceptance runs can be silently cancelled; release notes state facts

v3.0.1

Toggle v3.0.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #439 from hookdeck/fix/login-poll-path-guard

fix(login): stop the path guard rejecting the server's poll_url

v3.0.0

Toggle v3.0.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #368 from hookdeck/release/v3.0.0

v3.0.0: Outpost support, and MCP tools split into read and write halves

v2.6.0

Toggle v2.6.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #418 from hookdeck/docs/testing-practices-from-2.6.0

docs: record four testing practices the v2.6.0 release earned

v3.0.0-beta.1

Toggle v3.0.0-beta.1's commit message

Verified

This commit was signed with the committer’s verified signature.
leggetter Phil Leggetter
fix: report what a request retry actually did, and what a dismiss cha…

…nged

RetryRequest discarded the response body, so both surfaces reported a fixed
outcome. The MCP tool answered {"status":"retried"} and the CLI printed "retry
requested" whether the retry created events or matched nothing at all — a
retry aimed at a connection the request never went through is accepted and
produces nothing, and both said the same thing for it. The client now returns
the request and the events created; the MCP tool reports the event ids, and
the CLI fails when there are none.

The stub for that endpoint returned an empty body, which is not a response the
API sends, and the test asserted the hardcoded status — so it passed whether
or not a retry had produced anything. Both are corrected.

Also makes issue dismissal legible. Two independent QA passes reported
"dismiss does not dismiss" after checking status and finding it unchanged.
Verified against the API: DELETE returns 200 with dismissed_at populated and
status deliberately untouched, because dismissal and resolution are separate
axes. Nothing was broken, so nothing is changed except what is said — the CLI
now names dismissed_at and states that status is unchanged, and the client
records the finding so a third pass does not re-derive it. Switching to
PUT {"status":"IGNORED"}, which does change status, would have conflated the
two concepts and thrown away the distinction the API draws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wajsob136PyRh6nc92w5L3

v2.6.0-beta.2

Toggle v2.6.0-beta.2's commit message

Verified

This commit was signed with the committer’s verified signature.
leggetter Phil Leggetter
Merge remote-tracking branch 'origin/integrate/outpost-mcp' into feat…

…/outpost-api-client

v2.6.0-beta.1

Toggle v2.6.0-beta.1's commit message

Verified

This commit was signed with the committer’s verified signature.
leggetter Phil Leggetter
test(outpost): cover event get, attempt get, tenant token and retry

Raises CLI-level acceptance coverage from 22/30 to 26/30 leaf commands. All
four reuse data the existing tests already create, so they add coverage without
adding setup.

The tenant token assertion checks shape rather than contents — three JWT
segments, and that the raw tenant id is not readable in it. The token is a real
credential, so a test should not print or match on its payload.

The four commands still uncovered are the tenant portal and its custom domain.
They are not omitted casually: `custom-domain set` configures a real DNS-verified
hostname on the shared project, and `tenant portal` returns 404 until one exists.
Covering them safely needs a dedicated throwaway domain. They are the least
proven surface and should be called out as such in beta release notes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wajsob136PyRh6nc92w5L3

v2.5.0

Toggle v2.5.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix: v2.5.0 bug fixes across listen, config, and validation (#342)

* fix: make the CLI safe to run without a terminal

Six triaged issues plus three related bugs found while scoping them. They share
one failure shape: the CLI silently does something other than what the caller
asked, and the first symptom is missing traffic rather than an error.

Design rule applied throughout: a prompt must never be the only path. Without a
terminal, reads pick the safe default and writes fail loudly; rendering degrades
rather than dies; exit codes tell the truth.

- #332 `ci --local` / `login --local` no longer write the global config. The
  login flows saved the profile before --local was inspected, so the flag added
  a second write instead of redirecting the first, silently repointing the
  machine's active project. Gated at the single writeConfig chokepoint.

- #333 `listen` falls back to compact output when stdout is not a terminal. The
  interactive renderer opens /dev/tty itself, so in CI, Docker, nohup or an AI
  agent it died at startup and the command exited 0 having forwarded nothing.
  Renderer failures now propagate as a non-zero exit rather than being logged
  and discarded.

- #334 `listen` honours HOOKDECK_API_KEY. It previously fell through to a guest
  account: traffic arrived locally so it looked like it worked, but none of it
  was in the caller's project. HOOKDECK_API_KEY holds a Project API key, which
  /cli-auth/validate rejects with 401, so it is exchanged for a CLI client key
  via POST /cli-auth/ci exactly as `hookdeck ci` does, then saved. Precedence:
  --cli-key, stored login, HOOKDECK_API_KEY, guest.

- #335 Explicitly empty secret and identity flags are rejected. `"$UNSET_VAR"`
  expands to "", and hasAny() is a pure != "" test, so the value was dropped and
  the source created with no verification at all while looking configured. An
  empty value never cleared anything: the API expresses "no verification" as a
  null auth object, and `--config '{"auth": null}'` still does that.

- #336 The release skill's CI gate used the legacy commit-status API, which
  GitHub Actions never writes, so it returned pending off total_count 0 and
  blocked every release. Replaced with the GraphQL statusCheckRollup. The skill
  was also duplicated byte-for-byte under .agents/; that is now a symlink, as
  .claude/skills and .cursor/skills already were.

- #331 Dropped go-github v28, whose only use was one unauthenticated call and
  whose only other effect was linking x/crypto/openpgp through package init
  (GO-2026-5932, no upstream fix). Replaced with a net/http call, now testable
  and tested. Added a govulncheck job, since nothing enforced the scan.

Also fixed, previously unreported:

- Unauthenticated commands no longer hang. Any command failing for want of
  credentials dropped into interactive login: blocking on Enter, opening a
  browser, then polling for ~4 minutes. Now gated on a terminal, and reports how
  to authenticate without one.

- Destructive commands no longer exit 0 on a skipped confirmation. The five
  delete/dismiss commands called fmt.Scanln and discarded its error, so without
  a terminal they printed "cancelled" and returned nil: a CI job deleted nothing
  and reported success. They now fail and name --force.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

* test: cover the real-world usage each fix was reported from

The previous acceptance tests asserted configuration and error text. These
assert the outcomes users actually reported, and each one has been verified to
fail against unmodified main and pass on this branch.

- listen forwards a real event end to end with no terminal. Starts a local HTTP
  server, creates a source and CLI connection, runs `listen` with no --output,
  POSTs to the source URL and waits for the payload to arrive locally. This is
  the scenario #333 was found in: start a service, start a tunnel, send an
  event, check it arrives. Asserting only on the absence of a TTY error would
  have passed even if nothing was being forwarded.

- Unauthenticated commands fail fast. Asserts elapsed time, not just the error:
  against main this test takes 261s, which is the ~4 minute browser-login poll
  #337 describes. A message-only assertion would pass while still hanging.

- `ci --local` leaves the active project alone, checked through `whoami` rather
  than file contents. The reported symptom was not "a file changed", it was that
  every other hookdeck invocation on the machine silently moved project (#332).

- delete without --force now covered on destination, connection and
  transformation as well as source. The five commands share one confirmation
  helper, but each has to call it; a command that quietly stops doing so keeps
  working and just stops catching the bug (#338).

Issue dismiss is covered by unit tests only: it shares the same helper, and
creating a real issue takes ~40s of setup for a one-line wiring check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

* fix: address Copilot review — guest-profile precedence, test race, wording

Five review findings, all valid. One was a real bug.

- HOOKDECK_API_KEY was ignored once a guest profile existed. GuestLogin
  persists its key, so Profile.APIKey is non-empty after a single guest run and
  the "no credentials stored" test treated the throwaway account as a real
  login. Every later run then stayed on the guest project despite an exported
  Project API key — #334 again, one run later. A guest profile is now
  identified by guest_url alongside the key, and the environment key takes
  precedence over it while still losing to a real stored login. Replacing a
  guest profile is announced rather than silent, since discarding a sandbox
  link is exactly the kind of side effect this release is about surfacing.

- The acceptance helpers raced. os/exec writes subprocess output from its own
  goroutine while the tests read the buffer mid-run, so a bare bytes.Buffer
  could return partial output. Reads and writes now go through a mutex.

- "Check it is exported" misdiagnosed the reported cause. In #335 the secret
  was in a workspace .env that application code loaded but the shell never did,
  so the variable was not set in the invoking shell at all; exporting only
  matters for a variable that is set locally but not visible to child
  processes. The error and the README now say "set in this shell" and note that
  .env values are not loaded automatically.

- REFERENCE.md advertised --cli-key as a global flag. The root persistent flag
  was registered unhidden while --api-key beside it is hidden, and regenerating
  the docs surfaced it — contradicting README "CLI authentication keys" and
  AGENTS.md, which state authentication is command-specific. It is now hidden
  like --api-key: still functional for existing callers, no longer advertised.
  The command-specific `login --cli-key` and `listen --cli-key` remain
  documented.

Adds the guest-profile acceptance case the review asked for, using a synthetic
guest config so it stays deterministic and does not create a real guest account
on every CI run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

* refactor: make .agents/skills the canonical skills location

.agents/skills is the cross-harness location — it is not tied to any one tool,
so it is the right home for the real files. This inverts what landed earlier in
this branch, which kept the files at a root skills/ and pointed .agents/skills
at it.

- .agents/skills/hookdeck-cli-release/ now holds the files (tracked as a rename,
  so history is preserved).
- .claude/skills and .cursor/skills are symlinks to ../.agents/skills.
- Root skills/ is gone; there is one canonical directory and no second copy to
  drift out of sync, which was the underlying problem in #336.

The skill sits one directory deeper than before, so its relative links needed
../../ -> ../../../. All four (release workflow, .goreleaser/, .goreleaser/mac.yml,
README) verified to resolve. AGENTS.md, README.md and CLAUDE.md updated to point
at the new location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

* fix: address second Copilot pass — error context, help text, issue coverage

Three findings, all valid.

- The HOOKDECK_API_KEY guidance was being discarded. CILogin returns a 401
  APIError, and Execute classifies it with IsUnauthorizedError — which matches
  both through errors.As and through a "status code: 401" substring check — so
  any wrapping lost the message and the user saw the generic "API key is invalid
  or expired" instead of the one thing that helps: that HOOKDECK_API_KEY takes a
  Project API key and a CLI client key belongs in --cli-key. Added an
  actionableError type that Execute checks before its 401 handling, so a command
  that has already explained the failure keeps its message.

- listen's help documented a precedence it no longer follows. It said stored
  credentials always beat HOOKDECK_API_KEY, which is untrue for a guest profile
  since the review fix. Help text and README now state the exception and that
  replacing a guest profile discards the sandbox link. REFERENCE.md regenerated.

- issue dismiss was the only one of the five destructive commands without a
  non-interactive test. The earlier justification for skipping it — that
  creating a real issue costs ~40s of setup — was wrong: runIssueDismissCmd
  confirms before it calls DismissIssue, so a non-existent ID exercises the
  guard in ~2s. All five callers are now covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

* test: cover the actionableError classification ordering

The actionableError type added in 570b71f changes the root error path but had no
test. The assertion that matters is the precondition: a wrapped 401 IS still
recognised by IsUnauthorizedError, so it is purely Execute's case ordering that
keeps the specific guidance. Writing that down means the day the precondition
stops holding, the test says so and the extra case can be removed rather than
lingering as cargo.

Also covers the other direction: an unmarked error must still receive the
generic recovery message, or every 401 would print raw API text instead of
telling the user how to sign in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

* fix: identify the CLI to GitHub, and pin govulncheck

Two suppressed comments from the third Copilot pass. Neither is breaking today,
both are worth doing.

- The release check sent Go's default "Go-http-client/1.1". GitHub accepts it
  (verified: 200), but asks callers to identify themselves and applies rate
  limits per User-Agent — and go-github used to set one, so dropping it was an
  unintended regression from that removal. Now sends "hookdeck-cli/<version>".
  Built inline rather than via pkg/useragent, which imports pkg/version and
  would make this an import cycle.

- The govulncheck job installed @latest, so a new release could change behaviour
  or fail CI with no change to the repo. Pinned to v1.6.0. The vulnerability
  database is still fetched at run time, so newly disclosed issues are picked up
  without a version bump.

Verified end to end: a binary stamped 2.0.0 still detects v2.4.0 against the
real API, and govulncheck v1.6.0 reports no findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RRp3XjWGqiYSun29SSNVJt

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>