merge the stop hook into an existing settings.json - #500
Conversation
Merge handled permissions and PreToolUse but never called mergeStopHooks -- only MergeCodex did. chunk init on a repo that already had a .claude/settings.json therefore merged its commit hooks and dropped the Stop hook on the floor, leaving the repo looking configured while nothing ran chunk validate at session end. Every Stop assertion in the suite was on the MergeCodex path, which is how it survived. CircleCI-Public/circleci-cli is in exactly this state today; it is fixed directly in that repo rather than here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
292108c to
9bc7357
Compare
michael-webster
left a comment
There was a problem hiding this comment.
Can you add an acceptance test for this that confirms the behavior.
| // Merge hooks.Stop — replace the chunk-managed group by command. Without | ||
| // this a repo that already had a settings.json keeps its commit hooks but | ||
| // never gets the Stop hook, so validation stops running at session end. | ||
| mergeStopHooks(merged, generatedMap) |
There was a problem hiding this comment.
i don't think you need a new function to merge the stop hooks, merge hooks just wasn't handling the case probably. i'd extend merge hooks to also include the stop hook merging as well.
There was a problem hiding this comment.
from claude:
done in adbf765 - mergeStopHooks is gone and mergeHooks handles both hook types, so Merge and MergeCodex make one call instead of two.
kept the difference in how they merge, since it is not incidental: chunk owns the whole PreToolUse group (matched by matcher) but only a single Stop entry (matched by command), because a user may have their own entries in that same group and replacing the group would delete them. That is now documented on the one function instead of split across two, and the duplicated "create hooks if absent" block collapses into a hooksMap helper.
no behavior change — the existing tests pin both halves independently: six exercise generated settings carrying only PreToolUse, six only Stop, three both.
mergeStopHooks sat alongside mergeHooks as a second top-level step, so every caller had to know chunk writes two kinds of hooks and remember to call both. Merge and MergeCodex now make one call. The two hook types still merge differently — PreToolUse by group, Stop by entry so a user's siblings survive — which is now documented on the one function rather than split across two. The duplicated "create hooks if absent" block collapses into hooksMap, which stays lazy: adding an empty hooks object to settings that have none would count as a change and prompt the user over nothing. No behavior change. The halves stay independent, which the existing tests already pin from both sides: six exercise generated settings carrying only PreToolUse, six only Stop, three both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
claude summary on acceptance test: added being straight about this though: the acceptance test does not catch this bug. checked by reintroducing the defect - it still passes, only internal/cmd it's structural. writeSettings only calls Merge when a .claude/settings.json already exists, and that path is gated behind tui.Confirm → ErrNoTTY → falls back to writing settings.example.json. so fresh-repo init never reaches Merge, and the one acceptance-reachable existing-settings path doesn't either. getting there needs a PTY dep (none in the tree) or a --yes flag. both easy, both new surface area on a PR you already asked me to shrink — tell me which and I'll add it. the real guard is the cmd-layer test. it previously asserted only merged["hooks"] != nil, which is why this shipped: hooks was non-nil from the merged commit hooks while Stop was missing. now asserts per hook type. |
|
added the acceptance test does not catch this bug. checked by reintroducing the defect - acceptance test still passes, and only internal/cmd |
description
MergehandledpermissionsandPreToolUsebut never calledmergeStopHooks.JustMergeCodexdid.So
chunk initon a repo that already had a.claude/settings.jsonmerged its commit hooks and dropped theStophook on the floor. The repo ended up looking fully configured while nothing ranchunk validateat session end. EveryStopassertion in the suite was on theMergeCodexpath, which is how this survived.changes since last review
The fix started as one line in
Mergecalling the existingmergeStopHooks. Self-review found that landing it that way traded one silent-data-loss bug for another, somergeHooksnow owns hook entries rather than hook groups. That is a bigger diff than "one line," and it changes two behaviors beyond the original bug:CommitMatcheris the bare tool nameBash, not a command glob, so any team with a non-commitBashPreToolUse hook shares that group with chunk. Replacing the group wholesale — which is what the old code did, and what the one-line fix would have kept doing for PreToolUse — dropped their entry. Chunk's entries are identified byCommitIfFilterinstead, and a group still on the legacy matcher is treated as chunk's in full since its entries predate theiftag.chunk validateentries now collapse. The oldmergeStopHooksreplaced the whole group, which incidentally deduped it. Replacing a single entry in place does not, so stale duplicates would have survived every subsequent merge and run validation twice per session. Every owned entry is stripped before the generated one is reinserted, so re-runninginitconverges.Ownership is now symmetric across both hook types and both the Claude and Codex paths share it, which is what let
MergeCodexdiverge fromMergein the first place.tests
internal/settings/merge_test.gocovers the merge itself: addingStopto an existing file, replacing chunk's entry in place, leaving a user's ownStopgroup alone, preserving a user entry that shares chunk's group, the same for a user entry in theBashPreToolUse group, duplicate collapse across groups and matchers, and idempotency on each shared-group path.On the acceptance test asked for in review — one is added (
TestInitWritesFiringStopHook, plus a real Stop-hook assertion on thesettings.example.jsonfallback), but it is worth being explicit that it does not catch this bug, and I verified that by reintroducing the defect: the acceptance test still passes and onlyinternal/cmdTestWriteSettingsExistingMergeAppliedfails.The reason is structural.
writeSettingsonly callsMergewhen a.claude/settings.jsonalready exists, and that path is gated behindtui.Confirm, which returnsErrNoTTYunder a test harness and falls back to writingsettings.example.jsonfrom the generated bytes. So a fresh-repoinitnever reachesMerge, and the one acceptance-reachable existing-settings path never reaches it either. Covering the merge from an acceptance test needs either a PTY dependency (none in the tree today —kr/ptyis only an indirectgo.sumhash) or a non-interactive apply flag. Happy to add either if that is the bar; flagging it rather than implying the acceptance test guards the fix.The actual regression guard is at the
cmdlayer, where the write happens. That test previously asserted onlymerged["hooks"] != nil, which is exactly why the bug shipped:hookswas non-nil from the merged commit hooks whileStopwas absent. It now asserts per hook type.scope change since first review
This PR originally also added
chunk hook repair, Stop-hook reporting inchunk validateandchunk hook status, and avalidation.requireStopHookopt-out. Per review, that general-purpose repair tooling is out of scope - the ticket was to find the affected repos and fix them directly. All of it has been dropped; what's left is the root-cause bug fix.affected repos (fixed directly)
What I did: Swept all 594 non-archived, non-fork repos in
circleciandCircleCI-Publicvia the GitHub GraphQL API for.chunk/config.json, then checked each for a firing Stop hook. 9 repos have a chunk config; all 9 are sidecar-configured; 5 had no firing Stop hook. PRs are open on each:CircleCI-Public/circleci-cliStopblock — this bugcircleci/opentelemetry-collector-deploy.claude/settings.jsoncircleci/public-api-service.claude/settings.jsoncircleci/factory-bot.claude/settings.jsoncircleci/sandbox-provisioner.claude/settings.jsonAlready healthy:
soc-integrations,notifications,llmops-service,chunk-cli.Note that only
circleci-cliis explained by this merge bug — the other four never had a.claude/settings.jsonwritten at all, which is a separate gap.