Skip to content

Commit 6f7011a

Browse files
committed
fix: idempotent hook install must compare path, not substring
The earlier `contains("claude-session-track")` check skipped reinstall whenever ANY hook with that substring was present — even if it pointed at a now-defunct path. On Nix/NixOS this leaves stale /nix/store hashes in ~/.claude/settings.json across rebuilds: the previous derivation gets garbage-collected, the next tmux start sees the stale-but-named hook and bails, and Claude Code emits SessionStart:clear hook error bash: <gone-path>/hooks/claude-session-track.sh: No such file New behaviour: - Skip only when the EXACT current command (path-equal) is already installed. - Otherwise filter out any prior instance of this hook (any path, name-matched), drop entries whose hooks list went empty, and install the current path. Result: each tmux start self-heals stale entries left by an earlier derivation. Idempotent on the right dimension. Verified with three scenarios: 1. Settings file containing a stale-path hook → replaced with current path on next run. 2. Repeated invocations on a current-path settings file → no duplicates added (count stays at 1). 3. Empty {} settings file → fresh install creates both SessionStart and SessionEnd hooks. Same fix applied to the SessionEnd hook (symmetric structure).
1 parent a7691f3 commit 6f7011a

1 file changed

Lines changed: 27 additions & 6 deletions

File tree

tmux-assistant-resurrect.tmux

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,50 @@ install_claude_hooks() {
5050
return
5151
fi
5252

53-
# Install SessionStart hook if not present.
54-
# Use contains() for matching — tolerates quoting changes across versions
55-
# (e.g., upgrading from unquoted to quoted paths won't create duplicates).
56-
if ! jq -e '.hooks.SessionStart[]?.hooks[]? | select((.command // "") | contains("claude-session-track"))' "$settings" >/dev/null 2>&1; then
53+
# Install SessionStart hook, refreshing a stale path if one exists.
54+
#
55+
# The earlier check matched on the substring "claude-session-track" and
56+
# skipped install if any hook contained it. That left STALE paths in
57+
# place across reinstalls — e.g. on Nix/NixOS, where each rebuild can
58+
# produce a new /nix/store hash for the plugin and the previous
59+
# derivation gets garbage-collected. Claude Code then ran a hook
60+
# pointing at a path that no longer existed and emitted
61+
# "SessionStart:clear hook error / bash: <gone-path>: No such file".
62+
#
63+
# New idempotency: skip only when the EXACT current command (path-
64+
# matched) is already installed. Otherwise filter out any prior
65+
# instance of this hook (any path) and add the current one. Each
66+
# tmux start now self-heals stale entries left by a previous install.
67+
if ! jq -e --arg cmd "$track_cmd" '.hooks.SessionStart[]?.hooks[]? | select((.command // "") == $cmd)' "$settings" >/dev/null 2>&1; then
5768
local tmp
5869
tmp=$(mktemp)
5970
jq --arg cmd "$track_cmd" '
6071
.hooks //= {} |
6172
.hooks.SessionStart //= [] |
73+
# Drop any prior instance of this hook (different paths included).
74+
.hooks.SessionStart |= map(
75+
.hooks |= map(select((.command // "") | contains("claude-session-track") | not))
76+
) |
77+
# Drop entries whose hooks list became empty after the filter.
78+
.hooks.SessionStart |= map(select((.hooks // []) | length > 0)) |
6279
.hooks.SessionStart += [{
6380
"matcher": "",
6481
"hooks": [{"type": "command", "command": $cmd}]
6582
}]
6683
' "$settings" > "$tmp" && mv "$tmp" "$settings"
6784
fi
6885

69-
# Install SessionEnd hook if not present
70-
if ! jq -e '.hooks.SessionEnd[]?.hooks[]? | select((.command // "") | contains("claude-session-cleanup"))' "$settings" >/dev/null 2>&1; then
86+
# Install SessionEnd hook (same self-healing pattern as SessionStart).
87+
if ! jq -e --arg cmd "$cleanup_cmd" '.hooks.SessionEnd[]?.hooks[]? | select((.command // "") == $cmd)' "$settings" >/dev/null 2>&1; then
7188
local tmp
7289
tmp=$(mktemp)
7390
jq --arg cmd "$cleanup_cmd" '
7491
.hooks //= {} |
7592
.hooks.SessionEnd //= [] |
93+
.hooks.SessionEnd |= map(
94+
.hooks |= map(select((.command // "") | contains("claude-session-cleanup") | not))
95+
) |
96+
.hooks.SessionEnd |= map(select((.hooks // []) | length > 0)) |
7697
.hooks.SessionEnd += [{
7798
"matcher": "",
7899
"hooks": [{"type": "command", "command": $cmd}]

0 commit comments

Comments
 (0)