fix: dedup Claude / Codex hook entries by command-substring - #43
Conversation
Claude Code and Codex strip unknown fields (_marker, _version) when they normalize ~/.claude/settings.json and ~/.codex/hooks.json. Harness's dedup logic was matching on those sidecar fields, so on every re-install prior entries went unrecognized and a fresh copy was appended — the user's settings.json had each event installed three times. Match on the /tmp/harness-status substring baked into the hook command instead. The command survives normalization by definition, and the path is unique enough that no plausible user-authored hook would collide. Also drop the gating around installHooksGlobally() at boot — installHooks() is idempotent (strips old Harness entries then writes one), so calling it unconditionally now serves as the dedup sweep that collapses existing users' duplicates on next launch. Drops the now-dead _marker/_version constants and stops writing those fields on new entries. Adds tests covering the normalized-form detection, the install-twice-yields-one invariant, the duplicate collapse case, and user-authored hook preservation through install / uninstall. Flags the STATUS_DIR ↔ HARNESS_HOOK_COMMAND_SIGNATURE coupling in src/main/hooks.ts so a future rename catches both sides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
I noticed I had 3 copies of all the Harness hooks in ~/.claude/settings.json. This might line up with the number of times Harness has updated for me. Restarting Harness adds a duplicate hook, but I see the _marker and _version properties. I don't know what triggers it, but something rewrites setttings.json and strips out those properties. I think this might have caused issues with events getting lost. e.g., sometimes I'd find a worktree waiting for an answer, but the UI showed it still working. Since removing the duplicate hooks, I haven't noticed this problem. There are probably some better ways to deal with this:
Or maybe the TUI replacement you talked about in #31 would open up other options that don't rely on hooks? |
|
I just checked and I have like 50 hooks installed, great catch!
I suspect that the claude cli runtime itself reformats/sanitizes the settings.json. It uses that file for its own settings as well. I swear at some point the version/marker was working, but I haven't really been using that code path for a while, since I have been working on the JSON mode (tui replacement)
I don't know enough about plugins, but if there is a way to have plugins install hooks I would much prefer that to modifying the user's system. The hooks idea has always felt very hacky to me
The hook installation was a specific button for a while but I had a few complaints that things were not working, but things weren't working because the hooks weren't installed. Lol
The TUI Replacement (aka JSON mode, available in experimental settings right now) does not rely on hooks, it uses the raw JSONL format that |
|
@frenchie4111 are you up for doing a release with this? |
|
@big-guy done :) |
Claude Code and Codex strip unknown fields (_marker, _version) when they normalize ~/.claude/settings.json and ~/.codex/hooks.json. Harness's dedup logic was matching on those sidecar fields, so on every re-install prior entries went unrecognized and a fresh copy was appended — the user's settings.json had each event installed three times.
Match on the /tmp/harness-status substring baked into the hook command instead. The command survives normalization by definition, and the path is unique enough that no plausible user-authored hook would collide.
Also drop the gating around installHooksGlobally() at boot — installHooks() is idempotent (strips old Harness entries then writes one), so calling it unconditionally now serves as the dedup sweep that collapses existing users' duplicates on next launch.
Drops the now-dead _marker/_version constants and stops writing those fields on new entries. Adds tests covering the normalized-form detection, the install-twice-yields-one invariant, the duplicate collapse case, and user-authored hook preservation through install / uninstall. Flags the STATUS_DIR ↔ HARNESS_HOOK_COMMAND_SIGNATURE coupling in src/main/hooks.ts so a future rename catches both sides.