feat(system): feat(hooks): DPLAN-0167 hook testing framework + bootstrap fix — 20-test harness with direct+integration layers, hook execution logger, CWD guard verification across projects, setup.sh provider wiring update (global_prompt_loader + env vars + git deny rules), bootstrap.py removes dead PreToolUse/PostToolUse from project settings, hook READMEs at provider+project level

Co-Authored-By: @devpulse <devpulse@aipass>
This commit is contained in:
AIOSAI
2026-05-07 12:58:47 -07:00
co-authored by @devpulse
parent 8da48ddd46
commit b971d2161e
29 changed files with 1584 additions and 174 deletions
+56
View File
@@ -0,0 +1,56 @@
# Project-Level Hooks
These hooks are provisioned by `aipass init` and live in the project's
`.claude/settings.json`. They fire when CWD is inside this project.
## What fires and what doesn't
**UserPromptSubmit** hooks fire from project settings. These work:
- `branch_prompt_loader.py` — injects branch-specific prompt
- `email_notification.py` — shows unread email count
- `identity_injector.py` — injects branch identity from passport
**PreToolUse / PostToolUse** hooks are provisioned but **DO NOT FIRE** from
project-level settings. This is a Claude Code limitation (confirmed S122,
GitHub issue #36071). These scripts exist but are dead weight:
- `pre_edit_gate.py` — intended to block cross-branch writes (never runs)
- `auto_fix_diagnostics.py` — intended to run pyright+ruff (never runs)
- `subagent_stop_gate.py` — intended to check subagent files (never runs)
These same hooks DO fire from provider settings (`~/.claude/settings.json`)
where they are also wired. The provider copies handle all enforcement.
**PreCompact** hooks fire from project settings:
- `pre_compact.py` — injects recovery context after compaction
## CWD guard interaction
When this project has UserPromptSubmit hooks (it does), the provider-level
UserPromptSubmit hooks detect this and exit silently. This prevents the AIPass
global prompt from being injected into projects that manage their own context.
The provider-level PreToolUse/PostToolUse hooks still fire (they can only run
at provider level) — so enforcement (git_gate, pre_edit_gate, auto_fix) is
always active regardless of CWD.
## Testing
Provider-level test harness covers project-level behavior:
```bash
python3 $AIPASS_HOME/.claude/hooks/hook_test.py --direct
```
Tests include:
- `direct_provider_guards_for_init_project` — verifies provider hooks are
CWD-guarded when run from an aipass init project
- `direct_project_settings_schema` — validates project settings.json has
expected hooks and all referenced scripts exist
## Updating hooks
```bash
drone @cli aipass init update # Refresh managed project files to latest templates
```
## Related
See `$AIPASS_HOME/.claude/hooks/README.md` for the full hook system documentation.
+20
View File
@@ -125,6 +125,26 @@
"file": "tests/test_ping_sweep.py",
"standard": "encapsulation",
"reason": "Unit tests must import handlers directly to test them in isolation. Entry-point imports would defeat the purpose of unit testing."
},
{
"file": "apps/handlers/init/bootstrap.py",
"standard": "debug_print",
"reason": "The print() on line 159 is inside a generated shell command string (python3 -c), not a bare print call in this module's code."
},
{
"file": "apps/handlers/init/bootstrap.py",
"standard": "help_text",
"reason": "The python3 references are in generated shell commands (settings.json hook entries), not in user-facing help text."
},
{
"file": "apps/handlers/init/bootstrap.py",
"standard": "json_structure",
"reason": "bootstrap.py is Pure Python only (no module/prax/cli imports) by design — it must work during initial project setup before any AIPass services exist."
},
{
"file": "apps/handlers/init/bootstrap.py",
"standard": "log_visibility",
"reason": "bootstrap.py is Pure Python only (no module/prax/cli imports) by design — stdlib getLogger is correct here. prax system_logger requires AIPass to be installed, which hasn't happened at bootstrap time."
}
]
}
@@ -44,25 +44,24 @@ from aipass.aipass.apps.handlers.init import scaffold_content as sc
logger = logging.getLogger(__name__)
ENFORCEMENT_HOOKS = [
"auto_fix_diagnostics.py",
"pre_edit_gate.py",
"subagent_stop_gate.py",
"pre_compact.py",
]
INJECTOR_HOOKS = [
PROJECT_HOOKS = [
"branch_prompt_loader.py",
"email_notification.py",
"identity_injector.py",
"pre_compact.py",
]
HOOKS_TO_SHIP = ENFORCEMENT_HOOKS + INJECTOR_HOOKS
# These are shipped as reference copies but NOT wired in project settings.json
# because PreToolUse/PostToolUse/SubagentStop only fire from provider settings.
PROVIDER_ONLY_HOOKS = [
"auto_fix_diagnostics.py",
"pre_edit_gate.py",
"subagent_stop_gate.py",
]
HOOKS_TO_SHIP = PROJECT_HOOKS + PROVIDER_ONLY_HOOKS
HOOK_EVENTS: dict[str, str] = {
"auto_fix_diagnostics.py": "PostToolUse",
"pre_edit_gate.py": "PreToolUse",
"subagent_stop_gate.py": "Stop",
"pre_compact.py": "PreCompact",
"branch_prompt_loader.py": "UserPromptSubmit",
"email_notification.py": "UserPromptSubmit",
@@ -137,16 +136,17 @@ def _detect_aipass_home() -> str | None:
def _claude_settings(aipass_home: str | None = None) -> str:
"""Generate .claude/settings.json — hooks for prompt injection + enforcement.
"""Generate .claude/settings.json — hooks for prompt injection at project level.
Wires all AIPass hooks into their respective event types:
Only wires hooks that fire from project-level settings:
- UserPromptSubmit: global/local prompt injection + branch_prompt_loader,
email_notification, identity_injector
- PostToolUse: auto_fix_diagnostics
- PreToolUse: pre_edit_gate
- Stop: subagent_stop_gate
- PreCompact: pre_compact
PreToolUse/PostToolUse/SubagentStop hooks are NOT wired here — they only
fire from provider settings (~/.claude/settings.json). The scripts are
still shipped as reference copies. Provider wiring is handled by setup.sh.
Args:
aipass_home: Optional AIPass installation root to add as env.AIPASS_HOME.
"""
@@ -0,0 +1,56 @@
# Project-Level Hooks
These hooks are provisioned by `aipass init` and live in the project's
`.claude/settings.json`. They fire when CWD is inside this project.
## What fires and what doesn't
**UserPromptSubmit** hooks fire from project settings. These work:
- `branch_prompt_loader.py` — injects branch-specific prompt
- `email_notification.py` — shows unread email count
- `identity_injector.py` — injects branch identity from passport
**PreToolUse / PostToolUse** hooks are provisioned but **DO NOT FIRE** from
project-level settings. This is a Claude Code limitation (confirmed S122,
GitHub issue #36071). These scripts exist but are dead weight:
- `pre_edit_gate.py` — intended to block cross-branch writes (never runs)
- `auto_fix_diagnostics.py` — intended to run pyright+ruff (never runs)
- `subagent_stop_gate.py` — intended to check subagent files (never runs)
These same hooks DO fire from provider settings (`~/.claude/settings.json`)
where they are also wired. The provider copies handle all enforcement.
**PreCompact** hooks fire from project settings:
- `pre_compact.py` — injects recovery context after compaction
## CWD guard interaction
When this project has UserPromptSubmit hooks (it does), the provider-level
UserPromptSubmit hooks detect this and exit silently. This prevents the AIPass
global prompt from being injected into projects that manage their own context.
The provider-level PreToolUse/PostToolUse hooks still fire (they can only run
at provider level) — so enforcement (git_gate, pre_edit_gate, auto_fix) is
always active regardless of CWD.
## Testing
Provider-level test harness covers project-level behavior:
```bash
python3 $AIPASS_HOME/.claude/hooks/hook_test.py --direct
```
Tests include:
- `direct_provider_guards_for_init_project` — verifies provider hooks are
CWD-guarded when run from an aipass init project
- `direct_project_settings_schema` — validates project settings.json has
expected hooks and all referenced scripts exist
## Updating hooks
```bash
drone @cli aipass init update # Refresh managed project files to latest templates
```
## Related
See `$AIPASS_HOME/.claude/hooks/README.md` for the full hook system documentation.
@@ -0,0 +1,56 @@
# Project-Level Hooks
These hooks are provisioned by `aipass init` and live in the project's
`.claude/settings.json`. They fire when CWD is inside this project.
## What fires and what doesn't
**UserPromptSubmit** hooks fire from project settings. These work:
- `branch_prompt_loader.py` — injects branch-specific prompt
- `email_notification.py` — shows unread email count
- `identity_injector.py` — injects branch identity from passport
**PreToolUse / PostToolUse** hooks are provisioned but **DO NOT FIRE** from
project-level settings. This is a Claude Code limitation (confirmed S122,
GitHub issue #36071). These scripts exist but are dead weight:
- `pre_edit_gate.py` — intended to block cross-branch writes (never runs)
- `auto_fix_diagnostics.py` — intended to run pyright+ruff (never runs)
- `subagent_stop_gate.py` — intended to check subagent files (never runs)
These same hooks DO fire from provider settings (`~/.claude/settings.json`)
where they are also wired. The provider copies handle all enforcement.
**PreCompact** hooks fire from project settings:
- `pre_compact.py` — injects recovery context after compaction
## CWD guard interaction
When this project has UserPromptSubmit hooks (it does), the provider-level
UserPromptSubmit hooks detect this and exit silently. This prevents the AIPass
global prompt from being injected into projects that manage their own context.
The provider-level PreToolUse/PostToolUse hooks still fire (they can only run
at provider level) — so enforcement (git_gate, pre_edit_gate, auto_fix) is
always active regardless of CWD.
## Testing
Provider-level test harness covers project-level behavior:
```bash
python3 $AIPASS_HOME/.claude/hooks/hook_test.py --direct
```
Tests include:
- `direct_provider_guards_for_init_project` — verifies provider hooks are
CWD-guarded when run from an aipass init project
- `direct_project_settings_schema` — validates project settings.json has
expected hooks and all referenced scripts exist
## Updating hooks
```bash
drone @cli aipass init update # Refresh managed project files to latest templates
```
## Related
See `$AIPASS_HOME/.claude/hooks/README.md` for the full hook system documentation.