diff --git a/CHANGELOG.md b/CHANGELOG.md index 658d640e..b2b3b76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,37 @@ PyPI version — not the changelog header. ## [2026-07-05] +### Fixed + +- **HVTrust badge restored in the root README.** hvtracker corrected the + methodology v4.1 grade-computation bug (issue #109); the badge shows the + right grade again, so the temporary comment-out from earlier today is + reverted. + +- **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). + +- **Windows fresh installs get a working hook bridge.** setup.sh wrote the + Claude bridge command with `.venv/bin/python3` on every OS — but Windows + venvs put the interpreter at `.venv/Scripts/python.exe` and have no `bin/`, + so hooks on a fresh Windows install pointed at a nonexistent python and + would never fire. The bridge string is now OS-aware (bash passes + `IS_WINDOWS` into the hook-install step). @hooks assessed the rest of the + chain: `$AIPASS_HOME` expansion works on Windows because Claude Code runs + hooks via Git Bash (which must exist for setup.sh to have run), and the + bridge itself has zero POSIX assumptions — the interpreter path was the + only gap. Verified: both OS modes produce the right bridge string, merge + marker unchanged, custom-hook preservation intact. (assessed by @hooks) + ### Added - **`./aipass` — repo-root cold-clone launcher (DPLAN-0234 Strand B).** The diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccf041b3..0739712d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ PRs are welcome after an issue has been discussed. Keep changes focused -- one f - Python 3.10+ - Tests: `pytest` (4,900+ tests across the project) - Quality: AIPass uses its own standards system ([seedgo](src/aipass/seedgo/README.md)) for automated audits -- Run `./setup.sh` to bootstrap the full environment +- Run `./aipass install --no-init` to bootstrap the full environment (contributors work in the engine repo itself — no first-project scaffold needed) ## Questions? diff --git a/README.md b/README.md index e29f8694..13a07998 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [](https://codecov.io/gh/AIOSAI/AIPass) [](https://scorecard.dev/viewer/?uri=github.com/AIOSAI/AIPass) [](https://www.bestpractices.dev/projects/13095) - +[](https://hvtracker.net/agents/aipass)
@@ -232,7 +231,7 @@ AIPass is built and tested with **Claude Code** on Linux/WSL.
| [Claude Code](https://code.claude.com/docs) | `claude -p "prompt" --permission-mode bypassPermissions` | Fully tested |
| [Codex](https://github.com/openai/codex) | `codex exec "prompt" --dangerously-bypass-approvals-and-sandbox` | Experimental |
-setup.sh auto-detects which CLIs are installed and configures hooks for each.
+The installer (`./aipass install`, powered by setup.sh) auto-detects which CLIs are installed and configures hooks for each — merging with any hooks you've already wired, never overwriting them.
---
diff --git a/setup.sh b/setup.sh
index d58a5267..a4a90d93 100755
--- a/setup.sh
+++ b/setup.sh
@@ -633,7 +633,7 @@ if [ -f "$SCRIPT_DIR/src/aipass/hooks/apps/handlers/bridges/claude.py" ]; then
echo "Installing Claude Code hooks ..."
mkdir -p "$HOME/.claude"
- "$PYTHON" - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" << 'PYEOF'
+ "$PYTHON" - "$SCRIPT_DIR" "$CLAUDE_SETTINGS" "$IS_WINDOWS" << 'PYEOF'
import json
import os
import sys
@@ -641,11 +641,16 @@ from pathlib import Path
repo_root = sys.argv[1]
settings_path = Path(sys.argv[2])
+is_windows = len(sys.argv) > 3 and sys.argv[3] == "1"
# Bridge entry point — all hooks route through the engine via this bridge.
# Uses $AIPASS_HOME env var (injected into settings.env below) so the
-# settings file stays relocatable.
-bridge = "$AIPASS_HOME/.venv/bin/python3 $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py"
+# settings file stays relocatable. CC on Windows runs hooks via Git Bash
+# (which must exist for setup.sh to have run), so $VAR expansion works —
+# but the venv interpreter lives at Scripts/python.exe there, not bin/python3
+# (@hooks assessment, DPLAN-0234 Strand C).
+venv_python = ".venv/Scripts/python.exe" if is_windows else ".venv/bin/python3"
+bridge = f"$AIPASS_HOME/{venv_python} $AIPASS_HOME/src/aipass/hooks/apps/handlers/bridges/claude.py"
# Load existing settings or start fresh
if settings_path.exists():
@@ -657,7 +662,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 +698,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