From c377fabc54cb48e0510ad2c33d674a5f47320113 Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Tue, 16 Jun 2026 10:00:29 +0200 Subject: [PATCH 01/13] Use actions where possible --- .github/workflows/build.yml | 42 ++++++++++--------------------------- Makefile | 7 ++++--- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d10150..f6f8320 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,9 @@ on: branches: ["main"] workflow_dispatch: +env: + GO_VERSION: "1.25" + permissions: contents: read @@ -20,13 +23,14 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: ${{ env.GO_VERSION }} cache: true - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: args: --timeout=5m + - name: Vet run: make vet @@ -39,16 +43,14 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: ${{ env.GO_VERSION }} cache: true - name: Install dependencies run: make get.dependencies - name: Run govulncheck - run: | - go install golang.org/x/vuln/cmd/govulncheck@latest - govulncheck ./... + uses: golang/govulncheck-action@v1 test: name: Test @@ -60,12 +62,12 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: ${{ env.GO_VERSION }} cache: true - - name: Install dependencies - run: make get.dependencies + - name: Test run: make test.all + - name: Run codacy-coverage-reporter uses: codacy/codacy-coverage-reporter-action@v1.3.0 continue-on-error: true @@ -88,30 +90,8 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version: ${{ env.GO_VERSION }} cache: true - - name: Install dependencies - run: make get.dependencies - - # - name: Run tests with coverage - # run: | - # make test.report - # go test -race -coverprofile=coverage.txt -covermode=atomic ./... - - # - name: Upload Go test results - # uses: actions/upload-artifact@v4 - # with: - # name: Test-Results - # path: TestResults.json - - # - name: Upload coverage to Codecov - # if: matrix.os == 'ubuntu-24.04' && matrix.go-version == '1.24' - # uses: codecov/codecov-action@v4 - # with: - # file: ./coverage.txt - # flags: unittests - # fail_ci_if_error: false - - name: Build run: make build diff --git a/Makefile b/Makefile index 40344a8..c930d3a 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,9 @@ .PHONY: all build test vet clean -install.tools: +install.tools.local: go install golang.org/x/vuln/cmd/govulncheck@latest + brew install golangci-lint clean: go clean -x -i @@ -15,7 +16,7 @@ fmt: vet: fmt go vet ./cmd/... ./internal/... ./pkg/... -lint: +lint.local: golangci-lint run ./... deps.upgrade: @@ -25,7 +26,7 @@ deps.upgrade: deps.vendor: go mod vendor -deps.vulncheck: +deps.vulncheck.local: govulncheck ./... deps.nancy: From 837cd6f260ab2eab056de3695951aca8280819b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:08:48 +0000 Subject: [PATCH 02/13] fix: set go-version-input and go-package on govulncheck-action --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6f8320..d44f92e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,6 +51,9 @@ jobs: - name: Run govulncheck uses: golang/govulncheck-action@v1 + with: + go-version-input: ${{ env.GO_VERSION }} + go-package: ./... test: name: Test From 0f6e6d640ecb13ce7338aa52a87df28dd126dc01 Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Tue, 16 Jun 2026 10:19:05 +0200 Subject: [PATCH 03/13] ci: disable govulncheck internal checkout to fix duplicate Authorization header Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d44f92e..cef7e6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,6 +54,7 @@ jobs: with: go-version-input: ${{ env.GO_VERSION }} go-package: ./... + repo-checkout: false test: name: Test From a3476d18816ce7d3566a8d9607a11ccb05ff3a7c Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Tue, 16 Jun 2026 10:27:46 +0200 Subject: [PATCH 04/13] Ignore commit message tmp files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ba023a4..5d1e50e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ TestResults.json *.test *.out go.work +.commit_msg_tmp From bcdafe1adadfc545398fb06dc30ccc7437628ee7 Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Tue, 16 Jun 2026 11:05:09 +0200 Subject: [PATCH 05/13] chore: add dev loop hook scripts and remove completed design TODOs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three Claude Code hooks enforce the branch→TDD→verify→PR→review loop: - scripts/hooks/guard-branch.sh blocks edits on main/master - scripts/hooks/test-on-edit.sh runs make test after every .go edit - scripts/hooks/loop-reminder.sh injects next-step context at turn end Remove the six design/TODO_*.md files that were already merged as PRs #36–#41. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 - design/TODO_01_Fix_secret_error_message.md | 95 --------- ...O_02_Remove_panic_for_expected_failures.md | 181 ------------------ design/TODO_03_Use_io_Reader_in_config.md | 90 --------- design/TODO_05_Fix_repositoy_typo.md | 78 -------- design/TODO_06_Drop_log_Default.md | 78 -------- design/TODO_07_Remove_dead_test_fixture.md | 83 -------- scripts/hooks/guard-branch.sh | 14 ++ scripts/hooks/loop-reminder.sh | 27 +++ scripts/hooks/test-on-edit.sh | 11 ++ 10 files changed, 52 insertions(+), 607 deletions(-) delete mode 100644 design/TODO_01_Fix_secret_error_message.md delete mode 100644 design/TODO_02_Remove_panic_for_expected_failures.md delete mode 100644 design/TODO_03_Use_io_Reader_in_config.md delete mode 100644 design/TODO_05_Fix_repositoy_typo.md delete mode 100644 design/TODO_06_Drop_log_Default.md delete mode 100644 design/TODO_07_Remove_dead_test_fixture.md create mode 100755 scripts/hooks/guard-branch.sh create mode 100755 scripts/hooks/loop-reminder.sh create mode 100755 scripts/hooks/test-on-edit.sh diff --git a/CLAUDE.md b/CLAUDE.md index 90df018..724ba9e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,5 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - ## Commands ```bash diff --git a/design/TODO_01_Fix_secret_error_message.md b/design/TODO_01_Fix_secret_error_message.md deleted file mode 100644 index a0d41d7..0000000 --- a/design/TODO_01_Fix_secret_error_message.md +++ /dev/null @@ -1,95 +0,0 @@ -# TODO 01 — Fix secret error message - -**Status:** DONE — merged as PR #36 - -## Problem - -`pkg/onepassword/onepassword_client.go:27` formats the error with `secret` (always -empty at that point) instead of `secretPath`. Every error reads: - -``` -failed to read secret : -``` - -The path that failed is invisible. - -## Branch - -``` -fix/secret-error-message -``` - -## Files - -- `pkg/onepassword/onepassword_client.go` (production) -- `pkg/onepassword/onepassword_client_test.go` (test) - ---- - -## RED — Write the failing test first - -Add to `TestGetSecret` in `pkg/onepassword/onepassword_client_test.go`: - -```go -t.Run("should include the secret path in the error message", func(t *testing.T) { - client := cliClient{ - runner: createMockOnePasswordCommandRunner(t, nil, errors.New("op failed")), - } - - _, err := client.GetSecret(testSecretPath) - - assert.ErrorContains(t, err, testSecretPath) -}) -``` - -### Verify RED - -```bash -go test -v ./pkg/onepassword/... -run "TestGetSecret/should_include_the_secret_path" -``` - -Expected failure: - -``` -Error: "failed to read secret : op failed" does not contain "somepath" -``` - -If the test passes immediately — the bug is already fixed. Stop and investigate before continuing. - ---- - -## GREEN — Minimal fix - -`pkg/onepassword/onepassword_client.go:27` — change `secret` to `secretPath`: - -```go -// before -return "", fmt.Errorf("failed to read secret %s: %w", secret, err) - -// after -return "", fmt.Errorf("failed to read secret %s: %w", secretPath, err) -``` - -### Verify GREEN - -```bash -go test ./pkg/onepassword/... -``` - -All tests must pass. No new failures. - ---- - -## REFACTOR - -No cleanup needed. One-character fix. - ---- - -## Verification checklist - -- [ ] Watched the test fail before touching production code -- [ ] Failure was "does not contain" — not a compile error, not a wrong failure -- [ ] Changed only the format argument -- [ ] All `./pkg/onepassword/...` tests green -- [ ] `make test` still green diff --git a/design/TODO_02_Remove_panic_for_expected_failures.md b/design/TODO_02_Remove_panic_for_expected_failures.md deleted file mode 100644 index b4953c9..0000000 --- a/design/TODO_02_Remove_panic_for_expected_failures.md +++ /dev/null @@ -1,181 +0,0 @@ -# TODO 02 — Remove panic for expected failures - -**Status:** DONE — merged as PR #39 - -## Problem - -`githubSecretDistribution` uses `panic` for expected operational errors (config file -missing, secret push failure). The function always returns `true` or panics — the -`bool` return type is dead code. The `false` branch in `main.go` is unreachable. - -Panics produce stack traces for routine CLI errors, which is confusing for end users. - -## Branch - -``` -fix/panic-to-error -``` - -## Files - -- `cmd/github-distribute-secrets/github_distribute_secrets.go` -- `cmd/github-distribute-secrets/main.go` -- `cmd/github-distribute-secrets/main_test.go` -- `cmd/github-distribute-secrets/github_distribute_secrets_test.go` - ---- - -## RED — Write the failing test first - -Add to `TestGithubSecretDistribution` in `github_distribute_secrets_test.go`: - -```go -t.Run("should return error if reading config fails", func(t *testing.T) { - configFileReader := &MockConfigFileReader{expectedError: assert.AnError} - - err := githubSecretDistribution(configFileReader, &MockOnePasswordClient{}, &mockGithubClient{}, false) - - assert.ErrorIs(t, err, assert.AnError) -}) - -t.Run("should return error if applying config fails", func(t *testing.T) { - configFileReader := &MockConfigFileReader{ - expectedConfig: &config.Configuration{ - RawConfig: map[string]config.RepositoryConfiguration{"repo1": {"key": "val"}}, - Repositories: []string{"repo1"}, - }, - } - githubClient := &mockGithubClient{expectedError: assert.AnError} - - err := githubSecretDistribution(configFileReader, &MockOnePasswordClient{}, githubClient, false) - - assert.Error(t, err) -}) -``` - -### Verify RED - -```bash -go test ./cmd/github-distribute-secrets/... 2>&1 | head -20 -``` - -Expected: **compilation error** — `githubSecretDistribution` returns `bool`, not `error`. -That compilation failure is the RED phase. Do not proceed until you see it. - ---- - -## GREEN — Change the signature and fix callers - -### 1. `github_distribute_secrets.go` - -```go -func githubSecretDistribution( - configFileReader config.ConfigFileReader, - op onepassword.OnePasswordClient, - gh github.GithubClient, - dumpConfig bool, -) error { - configuration, err := configFileReader.ReadConfiguration("./config.yml") - if err != nil { - return fmt.Errorf("failed to read config file: %w", err) - } - - if dumpConfig { - fmt.Println(configuration.DumpConfiguration()) - } - - if !applyConfiguration(configuration, op, gh) { - return fmt.Errorf("configuration was not applied successfully") - } - - return nil -} -``` - -### 2. `main.go` - -Update the var type and call site: - -```go -var ( - // ... - myGithubSecretDistribution = githubSecretDistribution -) -``` - -The var type is inferred — it will update automatically once the function signature changes. - -Call site: - -```go -if err := myGithubSecretDistribution(myNewConfigFileReader(), op, gh, *dumpConfig); err != nil { - log.Fatalln(err) -} -``` - -### 3. `main_test.go` - -Update the mock closure type from `func(...) bool` to `func(...) error`: - -```go -myGithubSecretDistribution = func( - configFileReader config.ConfigFileReader, - op onepassword.OnePasswordClient, - gh github.GithubClient, - dumpConfig bool, -) error { - calledGithubSecretDistribution = true - return nil -} -``` - -Update all three places in `main_test.go` where the mock is assigned. - -### 4. `github_distribute_secrets_test.go` - -Replace `assert.Panics` tests with error-return assertions: - -```go -// before -t.Run("should panic even if a single application fails", func(t *testing.T) { - ... - assert.Panics(t, func() { - githubSecretDistribution(configFileReader, onePasswordClient, githubClient, false) - }) -}) - -// after -t.Run("should return error if a single application fails", func(t *testing.T) { - ... - err := githubSecretDistribution(configFileReader, onePasswordClient, githubClient, false) - assert.Error(t, err) -}) -``` - -Same for the "should panic if reading the config failed" test. - -### Verify GREEN - -```bash -go test ./cmd/github-distribute-secrets/... -``` - -All tests pass. No panics, no compile errors. - ---- - -## REFACTOR - -Remove the now-unused `"fmt"` import from `github_distribute_secrets.go` if it drops out, -or verify it stays (it's used in `fmt.Println`). - ---- - -## Verification checklist - -- [ ] Saw compilation failure before touching production code -- [ ] New tests use `assert.Error` / `assert.ErrorIs`, not `assert.Panics` -- [ ] Existing panic tests replaced (not deleted — replaced with error assertions) -- [ ] `main.go` call site updated — no `log.Fatalln` on `false` branch -- [ ] `make test` green -- [ ] `make build` compiles cleanly diff --git a/design/TODO_03_Use_io_Reader_in_config.md b/design/TODO_03_Use_io_Reader_in_config.md deleted file mode 100644 index c190b37..0000000 --- a/design/TODO_03_Use_io_Reader_in_config.md +++ /dev/null @@ -1,90 +0,0 @@ -# TODO 03 — Use `io.Reader` in `NewConfigFromReader` - -**Status:** DONE — merged as PR #37 - -## Problem - -`NewConfigFromReader` in `internal/config/config.go:44` accepts `*bytes.Reader` -instead of the `io.Reader` interface. The yaml decoder only needs `io.Reader`. -This unnecessarily prevents passing other reader types (files, HTTP responses, strings). - -## Branch - -``` -refactor/io-reader-interface -``` - -## Files - -- `internal/config/config.go` -- `internal/config/config_test.go` - ---- - -## RED — Write the failing test first - -Add to `TestNewConfigFromReader` in `internal/config/config_test.go`: - -```go -t.Run("should accept any io.Reader, not only bytes.Reader", func(t *testing.T) { - reader := strings.NewReader(yamlConfigurationCommonOnly) - - result, err := NewConfigFromReader(reader) - - assert.Nil(t, err) - assert.NotNil(t, result) -}) -``` - -Add `"strings"` to the test file's imports. - -### Verify RED - -```bash -go test ./internal/config/... 2>&1 | head -20 -``` - -Expected: **compilation error** — `*strings.Reader` cannot be used as `*bytes.Reader`. -That is the RED. Do not proceed until you see it. - ---- - -## GREEN — Widen the parameter type - -`internal/config/config.go:44` — change the parameter: - -```go -// before -func NewConfigFromReader(reader *bytes.Reader) (config *Configuration, err error) { - -// after -func NewConfigFromReader(reader io.Reader) (config *Configuration, err error) { -``` - -Add `"io"` to imports. The `"bytes"` import stays — it is used elsewhere in the file. - -No call-site changes needed. All existing callers pass `*bytes.Reader` which satisfies `io.Reader`. - -### Verify GREEN - -```bash -go test ./internal/config/... -``` - -All tests pass. - ---- - -## REFACTOR - -No cleanup needed. - ---- - -## Verification checklist - -- [ ] Saw compilation failure (`*strings.Reader` incompatible with `*bytes.Reader`) -- [ ] Only changed the parameter type — no other production code altered -- [ ] `"io"` added to imports, `"bytes"` import retained -- [ ] All `./internal/config/...` tests green -- [ ] `make test` green diff --git a/design/TODO_05_Fix_repositoy_typo.md b/design/TODO_05_Fix_repositoy_typo.md deleted file mode 100644 index 6607fa4..0000000 --- a/design/TODO_05_Fix_repositoy_typo.md +++ /dev/null @@ -1,78 +0,0 @@ -# TODO 05 — Fix `repositoy` typo - -**Status:** DONE — merged as PR #40 - -## Problem - -The parameter name `repositoy` (missing an `r`) appears in the `GithubClient` interface, -both implementations, and the `applyConfigurationToRepository` function. Parameter names -in Go interfaces don't affect callers, so this is not a breaking change. - -## Branch - -``` -refactor/fix-repositoy-typo -``` - -## Files - -- `pkg/github/github_client.go` -- `pkg/github/github_dry_run_client.go` -- `cmd/github-distribute-secrets/github_distribute_secrets.go` -- `cmd/github-distribute-secrets/github_distribute_secrets_test.go` - ---- - -## TDD note - -This is a pure rename with no behavior change. There is no failing test to write — -the behavior is identical before and after. The "test" is the full test suite -passing after the rename. - -**Rule:** Do not rename anything until you have confirmed the full suite is green on -`main`. That green suite is your baseline. Any failure after the rename is a regression. - ---- - -## Procedure - -### 1. Confirm baseline - -```bash -make test -``` - -All tests green. Record this as the baseline. - -### 2. Rename — do not touch anything else - -In every file listed above, replace every occurrence of `repositoy` with `repository`. - -Occurrences to rename (parameter names only — not strings or comments): - -| File | Location | -|------|----------| -| `pkg/github/github_client.go` | interface method signature (line ~10) | -| `pkg/github/github_client.go` | `cliGithubClient.AddSecretToRepository` param (line ~18) | -| `pkg/github/github_dry_run_client.go` | `dryRunGithubClient.AddSecretToRepository` param (line ~14) | -| `cmd/.../github_distribute_secrets.go` | `applyConfigurationToRepository` param + internal uses | -| `cmd/.../github_distribute_secrets_test.go` | `mockGithubClient.AddSecretToRepository` param | - -### 3. Verify nothing broke - -```bash -make test -``` - -Must be identical to baseline. Any new failure is a bug in the rename — fix it before -proceeding. - ---- - -## Verification checklist - -- [ ] Baseline `make test` green before starting -- [ ] Used find-replace, not manual edits — avoid missing an occurrence -- [ ] `grep -rn "repositoy" .` returns zero results after rename -- [ ] `make test` green after rename — same result as baseline -- [ ] `make build` compiles cleanly diff --git a/design/TODO_06_Drop_log_Default.md b/design/TODO_06_Drop_log_Default.md deleted file mode 100644 index afbdd18..0000000 --- a/design/TODO_06_Drop_log_Default.md +++ /dev/null @@ -1,78 +0,0 @@ -# TODO 06 — Drop `log.Default()` calls - -**Status:** DONE — merged as PR #41 - -## Problem - -`log.Default().Printf(...)`, `log.Default().Fatalln(...)`, and `log.Default().Panicf(...)` -are used throughout the codebase. `log.Default()` returns the same default logger as the -package-level functions — it is redundant noise. - -## Branch - -``` -refactor/drop-log-default -``` - -## Files - -- `cmd/github-distribute-secrets/github_distribute_secrets.go` -- `pkg/github/github_client.go` -- `cmd/github-distribute-secrets/main.go` - ---- - -## TDD note - -This is a pure cosmetic substitution with no behavior change. No failing test to write. -The verification is `make test` passing before and after — same as the typo fix. - ---- - -## Procedure - -### 1. Confirm baseline - -```bash -make test -``` - -All green. - -### 2. Replace — do not touch anything else - -In each file, replace `log.Default().` with nothing (i.e., use the package-level call directly): - -| Before | After | -|--------|-------| -| `log.Default().Printf(...)` | `log.Printf(...)` | -| `log.Default().Fatalln(...)` | `log.Fatalln(...)` | -| `log.Default().Panicf(...)` | `log.Panicf(...)` | - -Occurrences by file: - -**`github_distribute_secrets.go`** -- `log.Default().Printf("Cannot apply config to repository %s successfully!", ...)` → `log.Printf(...)` - -**`github_client.go`** -- `log.Default().Printf("In repository %s. Adding secret with key %s", ...)` → `log.Printf(...)` - -**`main.go`** -- `log.Default().Fatalln(...)` → `log.Fatalln(...)` - -### 3. Verify nothing broke - -```bash -make test -``` - -Identical to baseline. - ---- - -## Verification checklist - -- [ ] Baseline `make test` green before starting -- [ ] `grep -rn "log.Default()" .` returns zero results after change -- [ ] `make test` green after change -- [ ] `make build` compiles cleanly diff --git a/design/TODO_07_Remove_dead_test_fixture.md b/design/TODO_07_Remove_dead_test_fixture.md deleted file mode 100644 index 67d0aee..0000000 --- a/design/TODO_07_Remove_dead_test_fixture.md +++ /dev/null @@ -1,83 +0,0 @@ -# TODO 07 — Remove dead test fixture - -**Status:** DONE — merged as PR #38 - -## Problem - -`yamlConfigurationOverwrites` in `internal/config/config_test.go` is identical to -`yamlConfigurationFull` and is never referenced in any test. It is dead code that -creates false familiarity — a reader might assume it tests override behavior. - -## Branch - -``` -refactor/remove-dead-test-fixture -``` - -## Files - -- `internal/config/config_test.go` - ---- - -## TDD note - -Deleting an unused constant has no observable behavior. There is no failing test to write. -The verification is `make test` passing before and after — same pattern as the typo fix. - -If the constant were referenced, the test suite would fail to compile on deletion — that -compilation failure would serve as the RED phase. - ---- - -## Procedure - -### 1. Confirm the constant is unused - -```bash -grep -rn "yamlConfigurationOverwrites" . -``` - -Expected: exactly one result — the declaration itself. Zero usage sites. - -### 2. Confirm baseline - -```bash -go test ./internal/config/... -``` - -All green. - -### 3. Delete the constant - -Remove lines 21–28 from `internal/config/config_test.go`: - -```go -// delete this entire block -yamlConfigurationOverwrites = ` -common: - KEY0: VAL0 -repo1: - KEY1: VAL1 -repo2: - KEY2: VAL2 -` -``` - -### 4. Verify - -```bash -go test ./internal/config/... -``` - -Same result as baseline. No compile errors, no test failures. - ---- - -## Verification checklist - -- [ ] `grep` confirmed zero usage sites before deleting -- [ ] Baseline green before starting -- [ ] Deleted the declaration only — no other changes -- [ ] `go test ./internal/config/...` green after deletion -- [ ] `make test` still green diff --git a/scripts/hooks/guard-branch.sh b/scripts/hooks/guard-branch.sh new file mode 100755 index 0000000..368fcf3 --- /dev/null +++ b/scripts/hooks/guard-branch.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# PreToolUse hook: blocks Edit/Write on main or master branch. + +BRANCH=$(git branch --show-current 2>/dev/null) + +if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then + jq -n --arg b "$BRANCH" '{ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": ("On branch " + $b + ". Create a feature branch first.") + } + }' +fi diff --git a/scripts/hooks/loop-reminder.sh b/scripts/hooks/loop-reminder.sh new file mode 100755 index 0000000..4e05a2c --- /dev/null +++ b/scripts/hooks/loop-reminder.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Stop hook: injects the next loop step into Claude's context for the next turn. + +git rev-parse --git-dir >/dev/null 2>&1 || exit 0 + +BRANCH=$(git branch --show-current 2>/dev/null) +DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') +AHEAD=$(git log @{u}.. --oneline 2>/dev/null | wc -l | tr -d ' ') + +MSG="" + +if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then + MSG="On main branch — create a feature branch before editing files." +elif [ "$DIRTY" -gt 0 ] && [ "$AHEAD" -eq 0 ]; then + MSG="Uncommitted changes on '$BRANCH'. Next: /superpowers:verification-before-completion → /commit-commands:commit-push-pr" +elif [ "$AHEAD" -gt 0 ]; then + MSG="Unpushed commits on '$BRANCH'. Next: /commit-commands:commit-push-pr → /code-review" +fi + +[ -z "$MSG" ] && exit 0 + +jq -n --arg m "$MSG" '{ + "hookSpecificOutput": { + "hookEventName": "Stop", + "additionalContext": ("Loop: " + $m) + } +}' diff --git a/scripts/hooks/test-on-edit.sh b/scripts/hooks/test-on-edit.sh new file mode 100755 index 0000000..f630c9a --- /dev/null +++ b/scripts/hooks/test-on-edit.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# PostToolUse hook: runs make test after any .go file is edited. + +FILE=$(jq -r '.tool_input.file_path // empty') + +[ -z "$FILE" ] && exit 0 +echo "$FILE" | grep -qE '\.go$' || exit 0 + +cd "$(git rev-parse --show-toplevel)" +make test 2>&1 | tail -20 +exit 0 From d6665c64beea0002fbfefdc56da6d5da68ece262 Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Tue, 16 Jun 2026 11:08:31 +0200 Subject: [PATCH 06/13] fix: exclude untracked files from loop-reminder dirty check Co-Authored-By: Claude Sonnet 4.6 --- scripts/hooks/loop-reminder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/hooks/loop-reminder.sh b/scripts/hooks/loop-reminder.sh index 4e05a2c..aec2ff7 100755 --- a/scripts/hooks/loop-reminder.sh +++ b/scripts/hooks/loop-reminder.sh @@ -4,7 +4,7 @@ git rev-parse --git-dir >/dev/null 2>&1 || exit 0 BRANCH=$(git branch --show-current 2>/dev/null) -DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') +DIRTY=$(git status --porcelain 2>/dev/null | grep -v '^??' | wc -l | tr -d ' ') AHEAD=$(git log @{u}.. --oneline 2>/dev/null | wc -l | tr -d ' ') MSG="" From c9bcd7138fae11ea57cff6c1d551c907b98fe907 Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Tue, 16 Jun 2026 11:16:54 +0200 Subject: [PATCH 07/13] feat: add /watch-pr command and extend loop reminder with PR state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .claude/commands/watch-pr.md: slash command that waits for CI checks, reads Copilot/reviewer feedback, fixes warranted issues, and re-pushes until the PR is green and review-addressed - loop-reminder.sh: adds PR-open state — when CI/review is pending the hook now reminds to run /watch-pr instead of going silent - guard-branch.sh + loop-reminder.sh: scope branch guard to main only (drop master check per project convention) Co-Authored-By: Claude Sonnet 4.6 --- .claude/commands/watch-pr.md | 57 ++++++++++++++++++++++++++++++++++ scripts/hooks/guard-branch.sh | 2 +- scripts/hooks/loop-reminder.sh | 4 ++- 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .claude/commands/watch-pr.md diff --git a/.claude/commands/watch-pr.md b/.claude/commands/watch-pr.md new file mode 100644 index 0000000..c16d896 --- /dev/null +++ b/.claude/commands/watch-pr.md @@ -0,0 +1,57 @@ +# Watch PR — CI and Review Loop + +After opening a PR, wait for CI and Copilot/reviewer feedback, fix warranted issues, +and push until the PR is green and all review findings are addressed. + +## Step 1 — Identify the PR + +```bash +BRANCH=$(git branch --show-current) +gh pr list --head "$BRANCH" --state open --json number,url +``` + +If no open PR is found, stop and tell the user. + +## Step 2 — Wait for CI + +```bash +gh pr checks --watch --interval 15 +``` + +## Step 3 — Handle CI failures + +For each failed check: + +1. Fetch the failure output from the check run logs via `gh run view` +2. Diagnose the root cause +3. Fix the code +4. Run `make test` to verify locally +5. Push — then go back to Step 2 + +## Step 4 — Fetch review feedback + +```bash +gh pr view --json reviews,comments +``` + +For each finding: +- Evaluate whether it is correct and warranted +- If yes: apply the fix, run `make test` +- If no: note the reasoning — do not blindly apply every suggestion + +## Step 5 — Push and re-check + +If any fixes were made: + +```bash +git add -u +git commit -m "fix: address CI failures / review findings" +git push +``` + +Then go back to Step 2 to confirm CI still passes. + +## Done + +Report the PR as ready to merge when CI is green and all review findings are addressed or +explicitly dismissed with reasoning. diff --git a/scripts/hooks/guard-branch.sh b/scripts/hooks/guard-branch.sh index 368fcf3..15f5b5e 100755 --- a/scripts/hooks/guard-branch.sh +++ b/scripts/hooks/guard-branch.sh @@ -3,7 +3,7 @@ BRANCH=$(git branch --show-current 2>/dev/null) -if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then +if [ "$BRANCH" = "main" ]; then jq -n --arg b "$BRANCH" '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/scripts/hooks/loop-reminder.sh b/scripts/hooks/loop-reminder.sh index aec2ff7..6db1f3f 100755 --- a/scripts/hooks/loop-reminder.sh +++ b/scripts/hooks/loop-reminder.sh @@ -9,12 +9,14 @@ AHEAD=$(git log @{u}.. --oneline 2>/dev/null | wc -l | tr -d ' ') MSG="" -if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then +if [ "$BRANCH" = "main" ]; then MSG="On main branch — create a feature branch before editing files." elif [ "$DIRTY" -gt 0 ] && [ "$AHEAD" -eq 0 ]; then MSG="Uncommitted changes on '$BRANCH'. Next: /superpowers:verification-before-completion → /commit-commands:commit-push-pr" elif [ "$AHEAD" -gt 0 ]; then MSG="Unpushed commits on '$BRANCH'. Next: /commit-commands:commit-push-pr → /code-review" +elif PR_NUM=$(timeout 5 gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null) && [ -n "$PR_NUM" ]; then + MSG="Open PR #$PR_NUM on '$BRANCH'. Next: /watch-pr to monitor CI and reviews" fi [ -z "$MSG" ] && exit 0 From 52d6e8b35f51b8ae94762e96db2f85b5453be64f Mon Sep 17 00:00:00 2001 From: David Schmitz Date: Wed, 17 Jun 2026 11:04:53 +0200 Subject: [PATCH 08/13] fix: address Copilot review findings and add /ship command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - guard-branch.sh: add exit 0 so allow path never exits non-zero; fix comment - test-on-edit.sh: set -o pipefail preserves make exit code; guard cd against empty TOPLEVEL - Makefile: add lint, install.tools, deps.vulncheck aliases for backwards compat - .claude/commands/ship.md: new /ship command for full commit→PR→CI→review loop Dismissed: master branch suggestions (deliberate choice), brew guard (.local suffix signals macOS-only), duplicate setup-go in CI (needed by make get.dependencies). Co-Authored-By: Claude Sonnet 4.6 --- .claude/commands/ship.md | 123 ++++++++++++++++++++++++++++++++++ Makefile | 8 ++- scripts/hooks/guard-branch.sh | 4 +- scripts/hooks/test-on-edit.sh | 7 +- 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 .claude/commands/ship.md diff --git a/.claude/commands/ship.md b/.claude/commands/ship.md new file mode 100644 index 0000000..0a4ff2b --- /dev/null +++ b/.claude/commands/ship.md @@ -0,0 +1,123 @@ +# Ship — Full Delivery Loop + +Commit staged work, push, open a PR, then run the full CI + review loop until the PR is green and all findings are addressed or dismissed. + +## Phase 1 — Commit & PR + +### 1.1 Check state + +```bash +git status --porcelain | grep -v '^??' +git branch --show-current +``` + +If no tracked changes and no unpushed commits, check for an already-open PR and skip to Phase 2. + +### 1.2 Stage and commit + +```bash +git add -u +``` + +Write a conventional commit message (`feat:`, `fix:`, `chore:`, etc.) that describes the why, not the what. + +```bash +git commit -m ": " +``` + +### 1.3 Push + +```bash +git push -u origin HEAD +``` + +### 1.4 Create PR (or detect existing) + +```bash +PR_NUM=$(gh pr list --head "$(git branch --show-current)" --state open --json number --jq '.[0].number' 2>/dev/null) +``` + +If `PR_NUM` is empty, create one: + +```bash +gh pr create --title "" --body "<summary + test plan>" +PR_NUM=$(gh pr list --head "$(git branch --show-current)" --state open --json number --jq '.[0].number') +``` + +Capture `PR_NUM` — it is used in every subsequent step. + +--- + +## Phase 2 — CI Loop + +Repeat until all checks pass. + +### 2.1 Wait for checks + +```bash +gh pr checks "$PR_NUM" --watch --interval 15 +``` + +### 2.2 Handle failures + +For each failed check: + +1. Find the run ID: `gh pr checks "$PR_NUM" --json name,link` +2. Fetch failure logs: `gh run view <run-id> --log-failed` +3. Diagnose root cause +4. Fix the code +5. `make test` — must pass locally before pushing +6. Commit and push: + ```bash + git add -u + git commit -m "fix: <what was wrong>" + git push + ``` +7. Go back to 2.1 + +--- + +## Phase 3 — Review Loop + +Run once CI is fully green. + +### 3.1 Fetch inline comments (Copilot, reviewers) + +```bash +REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner') +gh api "repos/$REPO/pulls/$PR_NUM/comments" --jq '.[] | {path: .path, line: .line, body: .body}' +``` + +### 3.2 Fetch review summaries + +```bash +gh pr view "$PR_NUM" --json reviews --jq '.reviews[] | {author: .author.login, state: .state, body: .body}' +``` + +### 3.3 Evaluate each finding + +For each comment: +- **Warranted**: apply fix, run `make test` +- **Not warranted**: note the reasoning explicitly — do not apply blindly + +### 3.4 If fixes were made + +```bash +git add -u +git commit -m "fix: address review findings" +git push +``` + +Then go back to Phase 2 to confirm CI still passes with the fixes. + +--- + +## Done + +Report the PR as ready to merge when: +- All CI checks are green +- All review findings are addressed or explicitly dismissed with reasoning + +```bash +gh pr view "$PR_NUM" --json url,title,state +``` diff --git a/Makefile b/Makefile index c930d3a..24531f7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ .DEFAULT_GOAL := build -.PHONY: all build test vet clean +.PHONY: all build test vet clean install.tools lint deps.vulncheck + +install.tools: install.tools.local install.tools.local: go install golang.org/x/vuln/cmd/govulncheck@latest @@ -16,6 +18,8 @@ fmt: vet: fmt go vet ./cmd/... ./internal/... ./pkg/... +lint: lint.local + lint.local: golangci-lint run ./... @@ -26,6 +30,8 @@ deps.upgrade: deps.vendor: go mod vendor +deps.vulncheck: deps.vulncheck.local + deps.vulncheck.local: govulncheck ./... diff --git a/scripts/hooks/guard-branch.sh b/scripts/hooks/guard-branch.sh index 15f5b5e..58b73d1 100755 --- a/scripts/hooks/guard-branch.sh +++ b/scripts/hooks/guard-branch.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# PreToolUse hook: blocks Edit/Write on main or master branch. +# PreToolUse hook: blocks Edit/Write on the main branch. BRANCH=$(git branch --show-current 2>/dev/null) @@ -12,3 +12,5 @@ if [ "$BRANCH" = "main" ]; then } }' fi + +exit 0 diff --git a/scripts/hooks/test-on-edit.sh b/scripts/hooks/test-on-edit.sh index f630c9a..6d608b9 100755 --- a/scripts/hooks/test-on-edit.sh +++ b/scripts/hooks/test-on-edit.sh @@ -1,11 +1,14 @@ #!/usr/bin/env bash # PostToolUse hook: runs make test after any .go file is edited. +set -o pipefail FILE=$(jq -r '.tool_input.file_path // empty') [ -z "$FILE" ] && exit 0 echo "$FILE" | grep -qE '\.go$' || exit 0 -cd "$(git rev-parse --show-toplevel)" +TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) +[ -z "$TOPLEVEL" ] && exit 0 + +cd "$TOPLEVEL" make test 2>&1 | tail -20 -exit 0 From 6ac2c02aeeca0b90b70808e8f59d681a20f83dc8 Mon Sep 17 00:00:00 2001 From: David Schmitz <david.schmitz.privat@gmail.com> Date: Wed, 17 Jun 2026 11:07:58 +0200 Subject: [PATCH 09/13] fix: resolve shellcheck warnings in hook scripts - test-on-edit.sh: cd with || exit guard (SC2164) - loop-reminder.sh: use grep -c instead of grep|wc -l (SC2126); quote @{u}.. refspec (SC1083) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- scripts/hooks/loop-reminder.sh | 4 ++-- scripts/hooks/test-on-edit.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/hooks/loop-reminder.sh b/scripts/hooks/loop-reminder.sh index 6db1f3f..7b1c032 100755 --- a/scripts/hooks/loop-reminder.sh +++ b/scripts/hooks/loop-reminder.sh @@ -4,8 +4,8 @@ git rev-parse --git-dir >/dev/null 2>&1 || exit 0 BRANCH=$(git branch --show-current 2>/dev/null) -DIRTY=$(git status --porcelain 2>/dev/null | grep -v '^??' | wc -l | tr -d ' ') -AHEAD=$(git log @{u}.. --oneline 2>/dev/null | wc -l | tr -d ' ') +DIRTY=$(git status --porcelain 2>/dev/null | grep -cv '^??') +AHEAD=$(git log '@{u}..' --oneline 2>/dev/null | wc -l | tr -d ' ') MSG="" diff --git a/scripts/hooks/test-on-edit.sh b/scripts/hooks/test-on-edit.sh index 6d608b9..1250e5f 100755 --- a/scripts/hooks/test-on-edit.sh +++ b/scripts/hooks/test-on-edit.sh @@ -10,5 +10,5 @@ echo "$FILE" | grep -qE '\.go$' || exit 0 TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) [ -z "$TOPLEVEL" ] && exit 0 -cd "$TOPLEVEL" +cd "$TOPLEVEL" || exit make test 2>&1 | tail -20 From 7b9d15acceb9f9851f86e9d0b20e7b7d20e32cf7 Mon Sep 17 00:00:00 2001 From: David Schmitz <david.schmitz.privat@gmail.com> Date: Wed, 17 Jun 2026 11:22:51 +0200 Subject: [PATCH 10/13] fix: correct markdown formatting in command files Add blank lines around fenced code blocks (MD031) and before list items (MD032) in ship.md and watch-pr.md to satisfy Codacy static analysis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- .claude/commands/ship.md | 4 ++++ .claude/commands/watch-pr.md | 1 + 2 files changed, 5 insertions(+) diff --git a/.claude/commands/ship.md b/.claude/commands/ship.md index 0a4ff2b..919e6a5 100644 --- a/.claude/commands/ship.md +++ b/.claude/commands/ship.md @@ -68,11 +68,13 @@ For each failed check: 4. Fix the code 5. `make test` — must pass locally before pushing 6. Commit and push: + ```bash git add -u git commit -m "fix: <what was wrong>" git push ``` + 7. Go back to 2.1 --- @@ -97,6 +99,7 @@ gh pr view "$PR_NUM" --json reviews --jq '.reviews[] | {author: .author.login, s ### 3.3 Evaluate each finding For each comment: + - **Warranted**: apply fix, run `make test` - **Not warranted**: note the reasoning explicitly — do not apply blindly @@ -115,6 +118,7 @@ Then go back to Phase 2 to confirm CI still passes with the fixes. ## Done Report the PR as ready to merge when: + - All CI checks are green - All review findings are addressed or explicitly dismissed with reasoning diff --git a/.claude/commands/watch-pr.md b/.claude/commands/watch-pr.md index c16d896..cde7f13 100644 --- a/.claude/commands/watch-pr.md +++ b/.claude/commands/watch-pr.md @@ -35,6 +35,7 @@ gh pr view <number> --json reviews,comments ``` For each finding: + - Evaluate whether it is correct and warranted - If yes: apply the fix, run `make test` - If no: note the reasoning — do not blindly apply every suggestion From 77b85ee6832f641ec13790c0301fae515e2675fb Mon Sep 17 00:00:00 2001 From: David Schmitz <david.schmitz.privat@gmail.com> Date: Wed, 17 Jun 2026 11:27:50 +0200 Subject: [PATCH 11/13] fix: silence loop-reminder when open PR has all CI checks green MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the stop hook kept injecting /watch-pr on every turn even after CI was fully green. Now it only fires if CI has failing, pending, or action-required checks — stays silent once the PR is ready to merge. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- scripts/hooks/loop-reminder.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/loop-reminder.sh b/scripts/hooks/loop-reminder.sh index 7b1c032..dd20046 100755 --- a/scripts/hooks/loop-reminder.sh +++ b/scripts/hooks/loop-reminder.sh @@ -16,7 +16,10 @@ elif [ "$DIRTY" -gt 0 ] && [ "$AHEAD" -eq 0 ]; then elif [ "$AHEAD" -gt 0 ]; then MSG="Unpushed commits on '$BRANCH'. Next: /commit-commands:commit-push-pr → /code-review" elif PR_NUM=$(timeout 5 gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null) && [ -n "$PR_NUM" ]; then - MSG="Open PR #$PR_NUM on '$BRANCH'. Next: /watch-pr to monitor CI and reviews" + FAILING=$(timeout 10 gh pr checks "$PR_NUM" --json state --jq '[.[] | select(.state == "FAILURE" or .state == "PENDING" or .state == "ACTION_REQUIRED")] | length' 2>/dev/null) + if [ -n "$FAILING" ] && [ "$FAILING" -gt 0 ]; then + MSG="Open PR #$PR_NUM on '$BRANCH' — CI not yet green. Next: /watch-pr" + fi fi [ -z "$MSG" ] && exit 0 From 28c552e3c7f03e2ac55c86b6d0b774c8033a2185 Mon Sep 17 00:00:00 2001 From: David Schmitz <david.schmitz.privat@gmail.com> Date: Wed, 17 Jun 2026 15:32:32 +0200 Subject: [PATCH 12/13] fix: address remaining Copilot review findings on hook scripts - guard-branch.sh: add early exit when not in a git repo; extend branch check to cover both main and master - test-on-edit.sh: add errexit + nounset to complement existing pipefail; use explicit exit 1 when cd fails - loop-reminder.sh: extend branch check to cover master and interpolate the actual branch name into the reminder message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- scripts/hooks/guard-branch.sh | 6 ++++-- scripts/hooks/loop-reminder.sh | 4 ++-- scripts/hooks/test-on-edit.sh | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/scripts/hooks/guard-branch.sh b/scripts/hooks/guard-branch.sh index 58b73d1..9711a34 100755 --- a/scripts/hooks/guard-branch.sh +++ b/scripts/hooks/guard-branch.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash -# PreToolUse hook: blocks Edit/Write on the main branch. +# PreToolUse hook: blocks Edit/Write on the main or master branch. + +git rev-parse --git-dir >/dev/null 2>&1 || exit 0 BRANCH=$(git branch --show-current 2>/dev/null) -if [ "$BRANCH" = "main" ]; then +if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then jq -n --arg b "$BRANCH" '{ "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/scripts/hooks/loop-reminder.sh b/scripts/hooks/loop-reminder.sh index dd20046..3140558 100755 --- a/scripts/hooks/loop-reminder.sh +++ b/scripts/hooks/loop-reminder.sh @@ -9,8 +9,8 @@ AHEAD=$(git log '@{u}..' --oneline 2>/dev/null | wc -l | tr -d ' ') MSG="" -if [ "$BRANCH" = "main" ]; then - MSG="On main branch — create a feature branch before editing files." +if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then + MSG="On ${BRANCH} branch — create a feature branch before editing files." elif [ "$DIRTY" -gt 0 ] && [ "$AHEAD" -eq 0 ]; then MSG="Uncommitted changes on '$BRANCH'. Next: /superpowers:verification-before-completion → /commit-commands:commit-push-pr" elif [ "$AHEAD" -gt 0 ]; then diff --git a/scripts/hooks/test-on-edit.sh b/scripts/hooks/test-on-edit.sh index 1250e5f..7df78a3 100755 --- a/scripts/hooks/test-on-edit.sh +++ b/scripts/hooks/test-on-edit.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash # PostToolUse hook: runs make test after any .go file is edited. +set -o errexit +set -o nounset set -o pipefail FILE=$(jq -r '.tool_input.file_path // empty') @@ -10,5 +12,5 @@ echo "$FILE" | grep -qE '\.go$' || exit 0 TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) [ -z "$TOPLEVEL" ] && exit 0 -cd "$TOPLEVEL" || exit +cd "$TOPLEVEL" || exit 1 make test 2>&1 | tail -20 From 9c91aa01a1c7506f43f4d4e6cc1779fb6b8a49d3 Mon Sep 17 00:00:00 2001 From: David Schmitz <david.schmitz.privat@gmail.com> Date: Wed, 17 Jun 2026 15:41:27 +0200 Subject: [PATCH 13/13] fix: guard jq and git commands against errexit abort in test-on-edit hook With set -o errexit active, a failing jq call (invalid stdin) or git rev-parse (outside a repo) would terminate the hook immediately instead of falling through to the existing guards. Add || exit 0 so failures produce a clean no-op rather than an unexpected abort. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --- scripts/hooks/test-on-edit.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/hooks/test-on-edit.sh b/scripts/hooks/test-on-edit.sh index 7df78a3..401feda 100755 --- a/scripts/hooks/test-on-edit.sh +++ b/scripts/hooks/test-on-edit.sh @@ -4,13 +4,12 @@ set -o errexit set -o nounset set -o pipefail -FILE=$(jq -r '.tool_input.file_path // empty') +FILE=$(jq -r '.tool_input.file_path // empty' 2>/dev/null) || exit 0 [ -z "$FILE" ] && exit 0 echo "$FILE" | grep -qE '\.go$' || exit 0 -TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) -[ -z "$TOPLEVEL" ] && exit 0 +TOPLEVEL=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 cd "$TOPLEVEL" || exit 1 make test 2>&1 | tail -20