Skip to content

fix: dedup Claude / Codex hook entries by command-substring - #43

Merged
frenchie4111 merged 1 commit into
ness-dev:mainfrom
big-guy:hook-dedup-by-command-substring
May 21, 2026
Merged

fix: dedup Claude / Codex hook entries by command-substring#43
frenchie4111 merged 1 commit into
ness-dev:mainfrom
big-guy:hook-dedup-by-command-substring

Conversation

@big-guy

@big-guy big-guy commented May 21, 2026

Copy link
Copy Markdown
Collaborator

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.

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>
@big-guy

big-guy commented May 21, 2026

Copy link
Copy Markdown
Collaborator Author

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:

  1. Put a UUID in the hook's command so we're absolutely sure it's from Harness
  2. Package the hooks in a plugin and run claude with that plugin added (--plugin-dir ...)
  3. Make it an explicit button a user presses

Or maybe the TUI replacement you talked about in #31 would open up other options that don't rely on hooks?

@frenchie4111

Copy link
Copy Markdown
Collaborator

I just checked and I have like 50 hooks installed, great catch!

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 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 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:

  1. Put a UUID in the hook's command so we're absolutely sure it's from Harness
  2. Package the hooks in a plugin and run claude with that plugin added (--plugin-dir ...)

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

  1. Make it an explicit button a user presses

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

Or maybe the TUI replacement you talked about in #31 would open up other options that don't rely on hooks?

The TUI Replacement (aka JSON mode, available in experimental settings right now) does not rely on hooks, it uses the raw JSONL format that claude cli can output. However, it's unclear to me if in the long term we will be able to completely remove the need for the TUI. It seems like Anthropic is very adversarial against non-tui users

@big-guy

big-guy commented May 21, 2026

Copy link
Copy Markdown
Collaborator Author

@frenchie4111 are you up for doing a release with this?

@big-guy
big-guy deleted the hook-dedup-by-command-substring branch May 22, 2026 19:13
@frenchie4111

Copy link
Copy Markdown
Collaborator

@big-guy done :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants