Skip to content

merge_settings cannot remove a stale prompt-type hook group; duplicates accumulate #159

Description

@millsmillsymills

Problem

merge_settings() in scripts/install.sh cannot remove a stale prompt-type hook group, so one accumulates permanently in ~/.claude/settings.json.

The merge decides which groups it owns by testing whether any command in the group references /.claude/hooks/:

[ ($eh[$event] // [])[]
  | select(
      [ .hooks[]?.command // "" ]
      | map(contains("/.claude/hooks/")) | any | not
    )
]
+ ($nh[$event] // [])
| unique

A type: "prompt" hook has no command, so .hooks[]?.command // "" yields "", the any is false, and the group is kept as user-authored. The template's group is then appended.

The trailing unique only collapses groups that are byte-identical. When the template's Stop group is [prompt, command] and the stale group is [prompt] alone, the two are distinct objects and both survive — forever, across every re-merge.

Observed

On this machine hooks.Stop held two groups:

  1. [prompt] — orphan, prompt only
  2. [prompt, command(stop-check-clean-repo.sh)] — the template's

Both carried identical prompt text, so the end-of-turn review ran twice per turn. ./scripts/install.sh settings reported "already up to date -- skipping" because the merge is a fixed point in this state: re-running it reproduces the same two groups.

The code comment at scripts/install.sh:140-141 anticipates this case — "The final unique collapses identical groups, including the prompt-type Stop hook that carries no command for the ours-test to match" — but that only holds while the template's group is also prompt-only. Once a command hook was added alongside the prompt, the two stopped being identical and the orphan became permanent.

Suggested fix

Extend the ours-test so a group is recognized as ours when it has no command hooks but its prompt text matches a prompt in the template's group for the same event. Roughly: treat a group as ours if any command references /.claude/hooks/ or every hook in it is a prompt whose text appears in the template's groups for that event.

Worth adding a regression test that merges a [prompt]-only existing group against a [prompt, command] template group and asserts one group out, not two.

Workaround

Delete the orphan group by hand:

jq --slurpfile c ~/Desktop/Projects/claude-defaults/settings.json \
  '.hooks.Stop = $c[0].hooks.Stop' ~/.claude/settings.json > /tmp/s.json

Already applied here; hooks.Stop is back to a single group.

Related

Found while fixing #158 (permission-denial exceptions in the Stop review prompt).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions