diff --git a/CHANGELOG.md b/CHANGELOG.md index 658d640e..a8e98d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,20 @@ PyPI version — not the changelog header. ## [2026-07-05] +### Fixed + +- **Installer no longer destroys a user's custom Claude Code hooks (DPLAN-0234 + Strand C).** setup.sh used to write `settings["hooks"]` wholesale — anyone + with their own hooks in `~/.claude/settings.json` lost them on install or + re-run. Now it merges: every AIPass bridge entry (identified by the + `bridges/claude.py` marker) is refreshed, while user-wired hooks and custom + events are preserved. Verified against fixtures: custom hooks survive, stale + AIPass entries are replaced without duplicates, and the fresh-install output + is shape-identical to before (7 events, 6 UserPromptSubmit + 6 PreCompact + entries). Found while fire-testing a fresh install's hooks in Docker — all + 17 wired hook entries pass on a cold Linux clone (real kernel/navmap/branch + prompt bytes, git gate blocks, clean no-ops on empty state). + ### Added - **`./aipass` — repo-root cold-clone launcher (DPLAN-0234 Strand B).** The diff --git a/setup.sh b/setup.sh index d58a5267..b288bf19 100755 --- a/setup.sh +++ b/setup.sh @@ -657,7 +657,7 @@ else: # UserPromptSubmit: 5 separate entries (EventType:hook_name) to avoid output merging # PreToolUse, PostToolUse, SubagentStop, Stop, Notification: single aggregate entries # PreCompact: 3 hooks x 2 matchers (manual + auto) = 6 entries -settings["hooks"] = { +aipass_hooks = { "UserPromptSubmit": [ {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:tier0_kernel"}]}, {"hooks": [{"type": "command", "command": f"{bridge} UserPromptSubmit:navmap"}]}, @@ -693,6 +693,19 @@ settings["hooks"] = { ], } +# Merge, don't replace (DPLAN-0234 Strand C): refresh every AIPass bridge entry +# (identified by the bridges/claude.py marker) but preserve any hooks the user +# wired themselves. Re-runs stay idempotent; custom hooks survive reinstall. +existing_hooks = settings.get("hooks", {}) +merged_hooks = {} +for event in set(existing_hooks) | set(aipass_hooks): + user_entries = [ + entry for entry in existing_hooks.get(event, []) + if "bridges/claude.py" not in json.dumps(entry) + ] + merged_hooks[event] = aipass_hooks.get(event, []) + user_entries +settings["hooks"] = merged_hooks + # Inject AIPASS_HOME into env block so dispatched agents find AIPass env_block = settings.get("env", {}) env_block["AIPASS_HOME"] = repo_root