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:
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user