Polywave Claude Code Hooks
May 23, 2026 · View on GitHub
Enforcement and injection hooks for CLI-based Polywave agents. 23 hooks across SubagentStart, PreToolUse, PostToolUse, SubagentStop, UserPromptSubmit, and Stop events.
Hook Summary
Enforcement hooks (block protocol violations)
| Hook | Event | Matcher | Rule | Description |
|---|---|---|---|---|
| inject_worktree_env | SubagentStart | — | E43 | Sets 5 env vars (worktree path, agent ID, wave num, IMPL path, branch) |
| validate_agent_isolation | SubagentStart | — | E12 | Verifies wave agent running in correct worktree (exit 2 blocks start) |
| validate_worktree_isolation | SubagentStart | — | E12 | Phase 1: pwd+branch pattern check; Phase 2: exact branch via .polywave-agent-brief.md frontmatter |
| inject_bash_cd | PreToolUse | Bash | E43 | Auto-prepends cd $POLYWAVE_AGENT_WORKTREE && to bash commands |
| validate_write_paths | PreToolUse | Write|Edit | E43 | Blocks relative paths and paths outside worktree |
| block_git_stash | PreToolUse | Bash | — | Blocks git stash in wave-agent worktrees (hides work from merge verification) |
| verify_worktree_compliance | SubagentStop | — | E42/I5 | Verifies completion report and commits (warn-only) |
| check_scout_boundaries | PreToolUse | Write|Edit | I6 | Scouts can only write IMPL docs |
| block_claire_paths | PreToolUse | Write|Edit|Bash | — | Blocks .claire typo paths |
| check_wave_ownership | PreToolUse | Write|Edit|NotebookEdit | I1 | Wave agents write only owned files |
| auto_format_polywave_agent_names | PreToolUse | Agent | E44 | Validates/formats polywave agent names (fallback for brief metadata) |
| validate_agent_launch | PreToolUse | Agent | H5 | Pre-launch validation gate + agent type injection (see below) |
| validate_impl_on_write | PostToolUse | Write|Edit | E16 | Validates IMPL schema + brief accuracy (symbols, line refs, wave_reference_invalid) after write |
| check_git_ownership | PostToolUse | Bash | I1 | Catches git-level ownership violations |
| warn_stubs | PostToolUse | Write|Edit | H3 | Warns on stub patterns in written code |
| check_branch_drift | PostToolUse | Bash | H4 | Detects commits on wrong branch |
| validate_agent_completion | SubagentStop | — | E42/I1/I4/I5/E20 | Validates protocol compliance at agent completion (incl. stub consistency) |
| validate_scout_output | SubagentStop | — | E16/I4 | Validates IMPL schema after scout completes; runs validate --fix then blocks if errors remain |
Injection hooks (prepend reference content)
| Hook | Event | Matcher | Mechanism | Description |
|---|---|---|---|---|
| validate_agent_launch | PreToolUse | Agent | updatedInput | Injects conditional agent references (3 files) into subagent prompt; always-needed content inlined in agent definitions |
| inject_skill_context | UserPromptSubmit | — | additionalContext | Injects skill subcommand references into orchestrator context |
Observability hooks
| Hook | Event | Matcher | Description |
|---|---|---|---|
| emit_agent_completion | SubagentStop | — | Emits structured completion event for claudewatch/SSE (async, non-blocking) |
| polywave_orchestrator_stop | Stop | — | Warns when session ends with active IMPL in WAVE_PENDING/EXECUTING state (non-blocking) |
Injection Patterns
Two hooks handle progressive disclosure injection. They operate at different layers and use different Claude Code mechanisms — neither can substitute for the other.
Layer 1: Orchestrator injection (inject_skill_context, UserPromptSubmit)
Fires when the user submits a prompt. The inject-context script uses direct conditional matching against the prompt text, loads the matching reference files from the skill's references/ directory, and returns them as additionalContext — prepended to the orchestrator's context before it runs.
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": "..." } }
The script matches ^/polywave program and ^/polywave amend patterns with direct conditional logic (no YAML frontmatter parsing).
Coverage: /polywave program *, /polywave amend *. Only subcommand-anchored patterns are reliable here — keyword triggers false-positive against skill body content.
Layer 2: Subagent injection (validate_agent_launch, PreToolUse/Agent)
Fires when the orchestrator calls the Agent tool to launch a subagent. The hook detects agent type and calls the inject-agent-context script for conditional injection. Only 3 conditional references remain; all other agent type references are inlined in agent definitions.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "prompt": "<!-- injected: references/wave-agent-build-diagnosis.md -->\n...\n\n[original prompt]" }
}
}
Why updatedInput, not additionalContext: additionalContext in a PreToolUse hook adds content to the calling model's context (the orchestrator). updatedInput modifies the tool call parameters before execution — the only mechanism that reaches inside a subagent's initial prompt.
Conditional injection (3 references):
scout+--programin prompt → injectscout-program-contracts.mdwave-agent+baseline_verification_failedin prompt → injectwave-agent-build-diagnosis.mdwave-agent+frozen_contractsin prompt → injectwave-agent-program-contracts.md
All other agent types (critic-agent, planner, integration-agent) require no injection — their content is fully inlined in their agent definitions.
Dedup: Injection markers (<!-- injected: references/X.md -->) prevent double-injection if the orchestrator also manually prepended the reference.
The two-layer picture
User types: /polywave wave
│
▼
UserPromptSubmit → inject_skill_context
Target: orchestrator context (additionalContext)
Matches: ^/polywave program, ^/polywave amend
Mechanism: inject-context script with direct conditional logic
│
▼ orchestrator runs, calls Agent tool
PreToolUse/Agent → validate_agent_launch (checks 1-8 enforcement + conditional injection)
Target: subagent initial prompt (updatedInput)
Conditional injection only:
scout + "--program" → scout-program-contracts.md
wave-agent + baseline fail → wave-agent-build-diagnosis.md
wave-agent + frozen contracts → wave-agent-program-contracts.md
No injection for: critic-agent, planner, integration-agent (content inlined)
Installation
Automated (Recommended)
cd ~/code/polywave/implementations/claude-code/hooks
./install.sh
The installer:
- Creates symlinks in
~/.claude/agents/hooks/for all 22 hook scripts - Merges hook configs into
~/.claude/settings.json(preserves existing hooks) - Verifies installation and runs basic tests
Manual
See individual hook sections below for manual installation steps.
Hook 1: Scout Boundaries (I6)
PreToolUse — Blocks Scout Write/Edit operations outside docs/IMPL/IMPL-*.yaml
How It Works
- Claude Code calls the script before executing Write/Edit tools
- Script receives JSON on stdin with tool_name, agent_type, tool_input
- If agent_type != "scout" -> allow (exit 0)
- If tool_name not in [Write, Edit] -> allow (exit 0)
- If file_path matches
docs/IMPL/IMPL-*.yaml-> allow (exit 0) - Otherwise -> block (exit 2) with I6 violation message
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/check_scout_boundaries ~/.claude/agents/hooks/check_scout_boundaries -
Add to
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/check_scout_boundaries" } ] } ] } }
Testing
# Test valid path (should exit 0)
echo '{"tool_name":"Write","agent_type":"scout","tool_input":{"file_path":"docs/IMPL/IMPL-test.yaml"}}' | \
check_scout_boundaries
echo $? # Should be 0
# Test invalid path (should exit non-zero)
echo '{"tool_name":"Write","agent_type":"scout","tool_input":{"file_path":"src/main.go"}}' | \
check_scout_boundaries 2>&1
echo $? # Should be non-zero
Hook 2: IMPL Validation on Write (E16)
PostToolUse — Validates IMPL docs after every Write, blocks on schema errors.
How It Works
- Claude Code calls the script after a Write tool completes
- Script checks if the written file matches
docs/IMPL/IMPL-*.yaml(skips archived/complete/docs) - Runs
polywave-tools validate(read-only, no--fix) - If validation fails -> blocks with error list; agent must fix before continuing
- If
polywave-toolsorjqnot on PATH -> exits silently (non-blocking)
Defense-in-Depth
Three layers of IMPL validation:
| Layer | When | Mechanism |
|---|---|---|
| Scout self-validation (Step 16) | After Scout writes IMPL | Scout runs polywave-tools validate --fix |
| Orchestrator E16 | After Scout completes | Orchestrator runs polywave-tools validate --fix |
| PostToolUse hook | On every Write to IMPL doc | Hook runs polywave-tools validate (read-only) |
The hook is the hard enforcement layer — it fires even if the Scout skips Step 16.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_impl_on_write ~/.claude/agents/hooks/validate_impl_on_write -
Add to
~/.claude/settings.json:{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/validate_impl_on_write" } ] } ] } }
Testing
# Write a valid IMPL doc — should exit 0
echo '{"tool_input":{"file_path":"docs/IMPL/IMPL-test.yaml"}}' | validate_impl_on_write
echo $? # 0
# Archived docs are skipped — should exit 0
echo '{"tool_input":{"file_path":"docs/IMPL/complete/IMPL-old.yaml"}}' | validate_impl_on_write
echo $? # 0
Hook 3: Block .claire Paths
PreToolUse — Blocks Write/Edit/Bash operations targeting .claire paths (common model hallucination).
How It Works
- Claude Code calls the script before executing Write/Edit/Bash tools
- Script checks if the file_path or command contains
.claire - If
.clairefound -> block (exit 2) with suggestion to use.claudeinstead - Otherwise -> allow (exit 0)
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/block_claire_paths ~/.claude/agents/hooks/block_claire_paths -
Add to
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit|Bash", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/block_claire_paths" } ] } ] } }
Testing
# Valid .claude path — should exit 0
echo '{"tool_name":"Write","tool_input":{"file_path":".claude/settings.json"}}' | block_claire_paths
echo $? # 0
# Hallucinated .claire path — should exit non-zero
echo '{"tool_name":"Write","tool_input":{"file_path":".claire/settings.json"}}' | block_claire_paths 2>&1
echo $? # Should be non-zero
Hook 4: Wave Ownership (I1)
PreToolUse — Enforces disjoint file ownership for Wave agents (I1 invariant).
How It Works
- Claude Code calls the script before executing Write/Edit/NotebookEdit tools
- Script checks if
agent_typeindicates a Wave agent - Extracts the agent's owned files list from the IMPL doc
- If file_path is not in the owned files list -> block (exit 2)
- Otherwise -> allow (exit 0)
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/check_wave_ownership ~/.claude/agents/hooks/check_wave_ownership -
Add to
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit|NotebookEdit", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/check_wave_ownership" } ] } ] } }
Hook 5: Git Ownership (I1 Layer 2)
PostToolUse — Catches git-level modifications outside file ownership boundaries.
How It Works
- Claude Code calls the script after a Bash tool completes
- Script checks if the command was a git operation (add, commit, etc.)
- Extracts staged/modified files from git status
- Cross-references against the agent's owned files list
- If any staged file is outside ownership -> block (exit 2) with violation message
- If not a git command or all files are owned -> allow (exit 0)
This is a second layer of I1 enforcement. Hook 4 (check_wave_ownership) catches Write/Edit at the tool level; this hook catches git operations that could stage files outside ownership via Bash commands.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/check_git_ownership ~/.claude/agents/hooks/check_git_ownership -
Add to
~/.claude/settings.json:{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/check_git_ownership" } ] } ] } }
Hook 6: IMPL Path Validation (H2)
Note: The standalone check_impl_path hook has been superseded by validate_agent_launch (Hook 10), which combines IMPL path validation with pre-launch gate checks. IMPL path validation is now part of the unified agent launch validation flow.
Hook 7: Stub Warning (H3)
PostToolUse — Scans written/edited code for stub patterns and emits a non-blocking warning.
How It Works
- Claude Code calls the script after a Write or Edit tool completes
- Script scans the written content for stub patterns:
TODO,FIXMEpass(Python)raise NotImplementedError(Python)unimplemented!()(Rust)throw new Error("not implemented")(JavaScript/TypeScript)
- If stubs found -> exit 0 with JSON
additionalContextwarning (non-blocking) - If no stubs found -> exit 0 silently
This hook is non-blocking — it warns the agent but does not prevent the write. The agent is expected to complete stub implementations before committing.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/warn_stubs ~/.claude/agents/hooks/warn_stubs -
Add to
~/.claude/settings.json:{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/warn_stubs" } ] } ] } }
Testing
# Write with no stubs — should exit 0 with no output
echo '{"tool_name":"Write","tool_input":{"file_path":"main.go","content":"package main\nfunc main() {}"}}' | warn_stubs
echo $? # 0
# Write with TODO stub — should exit 0 with warning
echo '{"tool_name":"Write","tool_input":{"file_path":"main.go","content":"// TODO: implement this"}}' | warn_stubs
# Output: {"additionalContext": "Warning: stub detected in main.go ..."}
echo $? # 0
Hook 8: Branch Drift Detection (H4)
PostToolUse — Detects when a git commit is made on the wrong branch (e.g., main instead of a wave branch).
How It Works
- Claude Code calls the script after a Bash tool completes
- Script checks if the command was a
git commit - If not a git commit -> allow (exit 0, no further checks)
- If git commit detected, checks the current branch against the expected wave branch
- If on
mainormaster-> block (exit 2) with drift warning - If on wrong wave branch -> block (exit 2) with expected branch name
- If on correct branch -> allow (exit 0)
This prevents accidental commits to main or another agent's branch during Wave execution, which would violate branch isolation.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/check_branch_drift ~/.claude/agents/hooks/check_branch_drift -
Add to
~/.claude/settings.json:{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/check_branch_drift" } ] } ] } }
Testing
# Non-git command — should exit 0 (skipped)
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | check_branch_drift
echo $? # 0
# Git commit on correct branch — should exit 0
echo '{"tool_name":"Bash","tool_input":{"command":"git commit -m \"test\""}}' | check_branch_drift
echo $? # 0 (if on expected branch)
Hook 9: Pre-Launch Validation Gate + Agent Type Injection (H5)
PreToolUse — Full pre-launch validation gate (checks 1–8) plus agent type reference injection (checks 9+). Dual-purpose: enforcement and progressive disclosure injection for subagents.
Checks 1–8: Enforcement
- Polywave tag detection — Parse
[polywave:wave{N}:agent-{ID}]from description; non-Polywave agents pass through - IMPL path extraction — Extract
docs/IMPL/IMPL-*.yamlfrom agent prompt - IMPL file exists — Verify the IMPL doc exists on disk
- IMPL validation — Run
polywave-tools validate(if polywave-tools on PATH) - Agent in wave — Verify agent ID exists in the specified wave
- Ownership file match — Cross-reference
.polywave-ownership.jsonagent ID and wave - Branch verification — Verify worktree branch matches
polywave/{slug}/wave{N}-agent-{ID} - Scaffold check — Verify all scaffolds are committed (if any in IMPL doc)
- Scout conditional injection — Conditionally injects
scout-program-contracts.mdwhen--programappears in the prompt. Suitability gate and implementation process are now inlined inscout.md. Detection: fires if[polywave:scoutappears in description,subagent_type: scoutappears in prompt, or# Scout Agent: Pre-Flight Dependency Mappingappears in prompt. Dedup: uses HTML comment markers<!-- injected: references/scout-X.md -->to skip files already present in the prompt. - Wave-agent conditional injection — Conditionally injects
wave-agent-build-diagnosis.md(when baseline verification failed) andwave-agent-program-contracts.md(when frozen contracts present). Worktree isolation and completion report are now inlined inwave-agent.md. Detection: fires if[polywave:waveappears in description orsubagent_type: wave-agentappears in tool input. Dedup: uses HTML comment markers<!-- injected: references/wave-agent-X.md -->to skip files already present in the prompt. - Critic-agent — No injection. All content inlined in
critic-agent.md. - Planner — No injection. All content inlined in
planner.md.
Checks 9+: Agent Type Conditional Injection
After enforcement passes, dispatch on subagent_type and conditionally inject matching reference files via updatedInput.prompt. As of v0.56.0, always-needed references are inlined directly in agent definitions. Only 3 conditional references remain.
| subagent_type | Reference files injected |
|---|---|
scout | scout-program-contracts.md (only when --program in prompt) |
wave-agent | wave-agent-build-diagnosis.md (when baseline verification failed); wave-agent-program-contracts.md (when frozen contracts present) |
critic-agent | No injection — all content inlined in critic-agent.md |
planner | No injection — all content inlined in planner.md |
integration-agent | No injection — all content inlined in integration-agent.md |
| other | pass through (exit 0) |
Status: Checks 1–8 enforcement unchanged. Checks 9–10 reduced to conditional-only injection. Checks 11–13 are no-ops (agent content fully inlined).
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_agent_launch ~/.claude/agents/hooks/validate_agent_launch -
Add to
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Agent", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/validate_agent_launch" } ] } ] } }
Testing
# Non-Polywave agent launch — should exit 0 (allowed through)
echo '{"tool_name":"Agent","tool_input":{"prompt":"Do some work","description":"helper agent"}}' | validate_agent_launch
echo $? # 0
# Polywave agent launch with valid context — should exit 0
echo '{"tool_name":"Agent","agent_type":"wave-agent","tool_input":{"prompt":"IMPL doc: docs/IMPL/IMPL-feature.yaml","description":"[polywave:wave1:agent-A] implement feature"}}' | validate_agent_launch
echo $? # 0 (if all preconditions met)
# Polywave agent launch with missing IMPL — should exit 1
echo '{"tool_name":"Agent","agent_type":"wave-agent","tool_input":{"prompt":"no impl path here","description":"[polywave:wave1:agent-A] implement feature"}}' | validate_agent_launch 2>&1
echo $? # 1 (blocked: no IMPL path found)
Hook 10: Worktree Environment Injection (E43)
SubagentStart — Sets 5 environment variables for worktree-based agents at launch time.
How It Works
- Claude Code calls the script when a subagent launches (before first tool execution)
- Script parses agent description for
[polywave:wave{N}:agent-{ID}]tag - Extracts IMPL doc path from agent prompt or reads from
.polywave-state/active-impl - Determines worktree path from
.polywave-state/worktrees.jsonor returns empty if solo wave - Returns
updatedEnvironmentwith 5 variables:POLYWAVE_AGENT_WORKTREE: Absolute worktree path (empty if solo wave)POLYWAVE_AGENT_ID: Agent ID (e.g., "A", "B2")POLYWAVE_WAVE_NUMBER: Wave number (e.g., "1")POLYWAVE_IMPL_PATH: Absolute IMPL doc pathPOLYWAVE_BRANCH: Expected branch name (e.g., "polywave/feature/wave1-agent-A")
These variables are consumed by other E43 hooks (inject_bash_cd, validate_write_paths) and can be read by agents for debugging.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/inject_worktree_env ~/.claude/agents/hooks/inject_worktree_env -
Add to
~/.claude/settings.json:{ "hooks": { "SubagentStart": [ { "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/inject_worktree_env" } ] } ] } }
Testing
# Test with Polywave agent description
echo '{"description":"[polywave:wave1:agent-A] implement feature","prompt":"IMPL doc: /path/to/IMPL-feature.yaml"}' | inject_worktree_env
# Should return JSON with updatedEnvironment containing 5 variables
# Test with non-Polywave agent (should pass through)
echo '{"description":"helper agent","prompt":"Do some work"}' | inject_worktree_env
echo $? # 0 (no environment changes)
Hook 11: Bash CD Injection (E43)
PreToolUse — Auto-prepends cd $POLYWAVE_AGENT_WORKTREE && to bash commands when in worktree context.
How It Works
- Claude Code calls the script before executing a Bash tool
- Script checks if
POLYWAVE_AGENT_WORKTREEenvironment variable is set (injected by Hook 10) - If unset or empty -> pass through (exit 0, no modification)
- If command already starts with
cd $POLYWAVE_AGENT_WORKTREE-> pass through (no double-injection) - Otherwise -> return
updatedInputwith command modified tocd $POLYWAVE_AGENT_WORKTREE && <original command>
This eliminates the "Agent B leak" scenario where agents forget to use absolute paths and create files in the main repo instead of their worktree.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/inject_bash_cd ~/.claude/agents/hooks/inject_bash_cd -
Add to
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/inject_bash_cd" } ] } ] } }
Testing
# Test without worktree env (should pass through unchanged)
echo '{"tool_name":"Bash","tool_input":{"command":"echo hello"}}' | inject_bash_cd
# Should return original input
# Test with worktree env (should inject cd)
export POLYWAVE_AGENT_WORKTREE="/path/to/worktree"
echo '{"tool_name":"Bash","tool_input":{"command":"go test ./..."}}' | inject_bash_cd
# Should return: {"hookSpecificOutput": {"updatedInput": {"command": "cd /path/to/worktree && go test ./..."}}}
Hook 12: Write Path Validation (E43)
PreToolUse — Blocks Write/Edit operations with relative paths or paths outside worktree boundaries.
How It Works
- Claude Code calls the script before executing Write/Edit tools
- Script checks if
POLYWAVE_AGENT_WORKTREEenvironment variable is set - If unset or empty -> pass through (solo wave agents use different isolation)
- If
file_pathis relative (doesn't start with/) -> block (exit 2) with error message - If
file_pathdoesn't start with$POLYWAVE_AGENT_WORKTREE-> block (exit 2) with boundary violation message - Otherwise -> allow (exit 0)
This is the hard enforcement layer for worktree isolation, catching attempts to write outside boundaries even if bash cd injection failed.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_write_paths ~/.claude/agents/hooks/validate_write_paths -
Add to
~/.claude/settings.json:{ "hooks": { "PreToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/validate_write_paths" } ] } ] } }
Testing
# Test without worktree env (should pass through)
echo '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test.txt"}}' | validate_write_paths
echo $? # 0
# Test with relative path (should block)
export POLYWAVE_AGENT_WORKTREE="/Users/user/worktree"
echo '{"tool_name":"Write","tool_input":{"file_path":"relative/path.go"}}' | validate_write_paths 2>&1
echo $? # 2 (blocked)
# Test with path outside worktree (should block)
echo '{"tool_name":"Write","tool_input":{"file_path":"/Users/user/other/path.go"}}' | validate_write_paths 2>&1
echo $? # 2 (blocked)
# Test with valid worktree path (should allow)
echo '{"tool_name":"Write","tool_input":{"file_path":"/Users/user/worktree/pkg/module/file.go"}}' | validate_write_paths
echo $? # 0
Hook 13: Worktree Compliance Verification (E42/I5)
SubagentStop — Verifies completion report and commits exist when agent finishes (warn-only, non-blocking).
How It Works
- Claude Code calls the script when a subagent stops (after last tool execution)
- Script checks if
POLYWAVE_AGENT_IDandPOLYWAVE_IMPL_PATHenvironment variables are set - If unset -> pass through (non-Polywave agent)
- Reads IMPL doc and extracts completion report for the agent
- If completion report missing -> warn to stderr (exit 0, non-blocking)
- If
POLYWAVE_BRANCHis set, checks that the branch has at least one commit - If no commits found -> warn to stderr (exit 0, non-blocking)
- Otherwise -> exit 0 silently
This hook is warn-only because SubagentStop fires after the agent completes — blocking here would not prevent protocol violations, only prevent the agent from stopping. Warnings are logged for debugging and observability.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/verify_worktree_compliance ~/.claude/agents/hooks/verify_worktree_compliance -
Add to
~/.claude/settings.json:{ "hooks": { "SubagentStop": [ { "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/verify_worktree_compliance" } ] } ] } }
Testing
# Test without Polywave context (should pass through)
echo '{}' | verify_worktree_compliance
echo $? # 0
# Test with Polywave context but no completion report (should warn)
export POLYWAVE_AGENT_ID="A"
export POLYWAVE_IMPL_PATH="/path/to/IMPL-feature.yaml"
export POLYWAVE_BRANCH="polywave/feature/wave1-agent-A"
echo '{}' | verify_worktree_compliance 2>&1
# Should output warning to stderr but exit 0
echo $? # 0
Hook 14: Agent Completion Event Emission (E40)
SubagentStop — Emits structured completion event for observability systems (claudewatch, SSE streams).
How It Works
- Claude Code calls the script when a subagent stops (after last tool execution)
- Script extracts agent metadata (type, ID, wave number, IMPL path) from environment and description
- Emits a structured JSON event to stdout with completion timestamp, agent context, and session metadata
- Runs asynchronously and non-blocking — always exits 0 regardless of success/failure
- If observability systems are unavailable, fails silently without blocking agent completion
This hook provides structured data for external observability systems to track agent lifecycle events, analyze parallel execution patterns, and diagnose coordination issues. It does not affect protocol execution — purely for monitoring and analytics.
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/emit_agent_completion ~/.claude/agents/hooks/emit_agent_completion -
Add to
~/.claude/settings.json:{ "hooks": { "SubagentStop": [ { "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/emit_agent_completion" } ] } ] } }
Testing
# Test with Polywave agent context (should emit JSON event)
export POLYWAVE_AGENT_ID="A"
export POLYWAVE_WAVE_NUMBER="1"
export POLYWAVE_IMPL_PATH="/path/to/IMPL-feature.yaml"
echo '{"description":"[polywave:wave1:agent-A] implement feature"}' | emit_agent_completion
# Should output JSON event to stdout
# Test with non-Polywave agent (should exit silently)
echo '{"description":"helper agent"}' | emit_agent_completion
echo $? # 0 (no output)
Hook 15: Orchestrator Stop Warning
Stop — Warns when the session ends with an active Polywave orchestration in progress.
How It Works
- Claude Code calls the script when the session is about to stop
- Script checks
stop_hook_activefield — if true, exits 0 immediately (loop prevention) - Scans
docs/IMPL/IMPL-*.yamlfor any IMPL withstate: WAVE_PENDINGorstate: WAVE_EXECUTING - Checks if
.claude/worktrees/polywave/contains any active worktree directories - If either check finds active work -> emit
systemMessagewarning (non-blocking, exit 0) - If no active work -> exit 0 silently
This hook is warn-only — it never blocks the session from ending. The stop_hook_active
field prevents infinite re-trigger loops (Claude Code sets this to true on re-entrant calls).
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/polywave_orchestrator_stop ~/.claude/agents/hooks/polywave_orchestrator_stop -
Add to
~/.claude/settings.json:{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/polywave_orchestrator_stop" } ] } ] } }
Testing
# Test: stop_hook_active=true should produce no output and exit 0
echo '{"stop_hook_active":true,"session_id":"test"}' | polywave_orchestrator_stop
echo $? # 0, no output
# Test: no active IMPLs — should exit 0 silently
echo '{"stop_hook_active":false,"session_id":"test"}' | polywave_orchestrator_stop
echo $? # 0
Hook 16: Scout Output Validation (E16/I4)
SubagentStop — Validates the IMPL doc after the Scout completes, blocking if schema errors persist.
How It Works
- Claude Code calls the script when any subagent stops
- Script checks if agent description contains
[polywave:scouttag; non-scouts pass through - Locates the IMPL doc (via
.polywave-state/active-impl, description path extraction, or filesystem scan) - Runs
polywave-tools validate --fix(auto-corrects fixable issues: invalid gate types, unknown keys) - Runs
polywave-tools validateagain (read-only) to check if errors remain - If errors persist after fix -> block (exit 2) with detailed error output
- If IMPL is valid -> allow (exit 0)
Why This Hook Exists
Without it, invalid IMPL docs (wrong state enum like SCOUT_COMPLETE, file paths in Agent.dependencies, scaffold entries in file_ownership) reach the orchestrator. The orchestrator then hits validation failures during wave preparation, wasting time on issues the scout should have caught.
This hook creates a hard gate between "scout finishes" and "orchestrator proceeds."
Defense-in-Depth Position
Scout writes IMPL
|
PostToolUse: validate_impl_on_write (warns agent inline, non-blocking)
|
Scout finishes
|
SubagentStop: validate_scout_output (BLOCKS if IMPL invalid after --fix)
|
Orchestrator receives control
|
PreToolUse/Agent: validate_agent_launch (H5: validates before wave agent launch)
Manual Installation
-
Symlink:
ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_scout_output ~/.claude/agents/hooks/validate_scout_output -
Add to
~/.claude/settings.json:{ "hooks": { "SubagentStop": [ { "hooks": [ { "type": "command", "command": "$HOME/.claude/agents/hooks/validate_scout_output" } ] } ] } }
Testing
# Non-scout agent — should exit 0 (pass through)
echo '{"agent_description":"[polywave:wave1:agent-A] implement feature"}' | validate_scout_output
echo $? # 0
# Scout agent with valid IMPL — should exit 0
echo '{"agent_description":"[polywave:scout:feature] analyze repo"}' | validate_scout_output
echo $? # 0 (if IMPL doc exists and is valid)
# Scout agent with invalid IMPL — should exit 2
echo '{"agent_description":"[polywave:scout:feature] analyze repo"}' | validate_scout_output 2>&1
echo $? # 2 (if IMPL has unfixable schema errors)
Maintenance
- Version control: All hook scripts are tracked in the polywave repository
- Updates:
git pullupdates the scripts via symlink - Dependencies: bash, jq, polywave-tools (graceful degradation if missing)
- Errors: Print to stderr and return exit code 2 (block) or 0 (allow)
- Non-blocking warnings: Return exit code 0 with JSON
additionalContexton stdout - Execution: Runs synchronously (blocks tool execution if it exits non-zero)
- Idempotent: Running
install.shmultiple times is safe (updates existing symlinks)