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)

HookEventMatcherRuleDescription
inject_worktree_envSubagentStartE43Sets 5 env vars (worktree path, agent ID, wave num, IMPL path, branch)
validate_agent_isolationSubagentStartE12Verifies wave agent running in correct worktree (exit 2 blocks start)
validate_worktree_isolationSubagentStartE12Phase 1: pwd+branch pattern check; Phase 2: exact branch via .polywave-agent-brief.md frontmatter
inject_bash_cdPreToolUseBashE43Auto-prepends cd $POLYWAVE_AGENT_WORKTREE && to bash commands
validate_write_pathsPreToolUseWrite|EditE43Blocks relative paths and paths outside worktree
block_git_stashPreToolUseBashBlocks git stash in wave-agent worktrees (hides work from merge verification)
verify_worktree_complianceSubagentStopE42/I5Verifies completion report and commits (warn-only)
check_scout_boundariesPreToolUseWrite|EditI6Scouts can only write IMPL docs
block_claire_pathsPreToolUseWrite|Edit|BashBlocks .claire typo paths
check_wave_ownershipPreToolUseWrite|Edit|NotebookEditI1Wave agents write only owned files
auto_format_polywave_agent_namesPreToolUseAgentE44Validates/formats polywave agent names (fallback for brief metadata)
validate_agent_launchPreToolUseAgentH5Pre-launch validation gate + agent type injection (see below)
validate_impl_on_writePostToolUseWrite|EditE16Validates IMPL schema + brief accuracy (symbols, line refs, wave_reference_invalid) after write
check_git_ownershipPostToolUseBashI1Catches git-level ownership violations
warn_stubsPostToolUseWrite|EditH3Warns on stub patterns in written code
check_branch_driftPostToolUseBashH4Detects commits on wrong branch
validate_agent_completionSubagentStopE42/I1/I4/I5/E20Validates protocol compliance at agent completion (incl. stub consistency)
validate_scout_outputSubagentStopE16/I4Validates IMPL schema after scout completes; runs validate --fix then blocks if errors remain

Injection hooks (prepend reference content)

HookEventMatcherMechanismDescription
validate_agent_launchPreToolUseAgentupdatedInputInjects conditional agent references (3 files) into subagent prompt; always-needed content inlined in agent definitions
inject_skill_contextUserPromptSubmitadditionalContextInjects skill subcommand references into orchestrator context

Observability hooks

HookEventMatcherDescription
emit_agent_completionSubagentStopEmits structured completion event for claudewatch/SSE (async, non-blocking)
polywave_orchestrator_stopStopWarns 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 + --program in prompt → inject scout-program-contracts.md
  • wave-agent + baseline_verification_failed in prompt → inject wave-agent-build-diagnosis.md
  • wave-agent + frozen_contracts in prompt → inject wave-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

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

  1. Claude Code calls the script before executing Write/Edit tools
  2. Script receives JSON on stdin with tool_name, agent_type, tool_input
  3. If agent_type != "scout" -> allow (exit 0)
  4. If tool_name not in [Write, Edit] -> allow (exit 0)
  5. If file_path matches docs/IMPL/IMPL-*.yaml -> allow (exit 0)
  6. Otherwise -> block (exit 2) with I6 violation message

Manual Installation

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/check_scout_boundaries ~/.claude/agents/hooks/check_scout_boundaries
    
  2. 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

  1. Claude Code calls the script after a Write tool completes
  2. Script checks if the written file matches docs/IMPL/IMPL-*.yaml (skips archived /complete/ docs)
  3. Runs polywave-tools validate (read-only, no --fix)
  4. If validation fails -> blocks with error list; agent must fix before continuing
  5. If polywave-tools or jq not on PATH -> exits silently (non-blocking)

Defense-in-Depth

Three layers of IMPL validation:

LayerWhenMechanism
Scout self-validation (Step 16)After Scout writes IMPLScout runs polywave-tools validate --fix
Orchestrator E16After Scout completesOrchestrator runs polywave-tools validate --fix
PostToolUse hookOn every Write to IMPL docHook runs polywave-tools validate (read-only)

The hook is the hard enforcement layer — it fires even if the Scout skips Step 16.

Manual Installation

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_impl_on_write ~/.claude/agents/hooks/validate_impl_on_write
    
  2. 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

  1. Claude Code calls the script before executing Write/Edit/Bash tools
  2. Script checks if the file_path or command contains .claire
  3. If .claire found -> block (exit 2) with suggestion to use .claude instead
  4. Otherwise -> allow (exit 0)

Manual Installation

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/block_claire_paths ~/.claude/agents/hooks/block_claire_paths
    
  2. 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

  1. Claude Code calls the script before executing Write/Edit/NotebookEdit tools
  2. Script checks if agent_type indicates a Wave agent
  3. Extracts the agent's owned files list from the IMPL doc
  4. If file_path is not in the owned files list -> block (exit 2)
  5. Otherwise -> allow (exit 0)

Manual Installation

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/check_wave_ownership ~/.claude/agents/hooks/check_wave_ownership
    
  2. 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

  1. Claude Code calls the script after a Bash tool completes
  2. Script checks if the command was a git operation (add, commit, etc.)
  3. Extracts staged/modified files from git status
  4. Cross-references against the agent's owned files list
  5. If any staged file is outside ownership -> block (exit 2) with violation message
  6. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/check_git_ownership ~/.claude/agents/hooks/check_git_ownership
    
  2. 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

  1. Claude Code calls the script after a Write or Edit tool completes
  2. Script scans the written content for stub patterns:
    • TODO, FIXME
    • pass (Python)
    • raise NotImplementedError (Python)
    • unimplemented!() (Rust)
    • throw new Error("not implemented") (JavaScript/TypeScript)
  3. If stubs found -> exit 0 with JSON additionalContext warning (non-blocking)
  4. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/warn_stubs ~/.claude/agents/hooks/warn_stubs
    
  2. 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

  1. Claude Code calls the script after a Bash tool completes
  2. Script checks if the command was a git commit
  3. If not a git commit -> allow (exit 0, no further checks)
  4. If git commit detected, checks the current branch against the expected wave branch
  5. If on main or master -> block (exit 2) with drift warning
  6. If on wrong wave branch -> block (exit 2) with expected branch name
  7. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/check_branch_drift ~/.claude/agents/hooks/check_branch_drift
    
  2. 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

  1. Polywave tag detection — Parse [polywave:wave{N}:agent-{ID}] from description; non-Polywave agents pass through
  2. IMPL path extraction — Extract docs/IMPL/IMPL-*.yaml from agent prompt
  3. IMPL file exists — Verify the IMPL doc exists on disk
  4. IMPL validation — Run polywave-tools validate (if polywave-tools on PATH)
  5. Agent in wave — Verify agent ID exists in the specified wave
  6. Ownership file match — Cross-reference .polywave-ownership.json agent ID and wave
  7. Branch verification — Verify worktree branch matches polywave/{slug}/wave{N}-agent-{ID}
  8. Scaffold check — Verify all scaffolds are committed (if any in IMPL doc)
  9. Scout conditional injection — Conditionally injects scout-program-contracts.md when --program appears in the prompt. Suitability gate and implementation process are now inlined in scout.md. Detection: fires if [polywave:scout appears in description, subagent_type: scout appears in prompt, or # Scout Agent: Pre-Flight Dependency Mapping appears in prompt. Dedup: uses HTML comment markers <!-- injected: references/scout-X.md --> to skip files already present in the prompt.
  10. Wave-agent conditional injection — Conditionally injects wave-agent-build-diagnosis.md (when baseline verification failed) and wave-agent-program-contracts.md (when frozen contracts present). Worktree isolation and completion report are now inlined in wave-agent.md. Detection: fires if [polywave:wave appears in description or subagent_type: wave-agent appears in tool input. Dedup: uses HTML comment markers <!-- injected: references/wave-agent-X.md --> to skip files already present in the prompt.
  11. Critic-agent — No injection. All content inlined in critic-agent.md.
  12. 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_typeReference files injected
scoutscout-program-contracts.md (only when --program in prompt)
wave-agentwave-agent-build-diagnosis.md (when baseline verification failed); wave-agent-program-contracts.md (when frozen contracts present)
critic-agentNo injection — all content inlined in critic-agent.md
plannerNo injection — all content inlined in planner.md
integration-agentNo injection — all content inlined in integration-agent.md
otherpass 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_agent_launch ~/.claude/agents/hooks/validate_agent_launch
    
  2. 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

  1. Claude Code calls the script when a subagent launches (before first tool execution)
  2. Script parses agent description for [polywave:wave{N}:agent-{ID}] tag
  3. Extracts IMPL doc path from agent prompt or reads from .polywave-state/active-impl
  4. Determines worktree path from .polywave-state/worktrees.json or returns empty if solo wave
  5. Returns updatedEnvironment with 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 path
    • POLYWAVE_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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/inject_worktree_env ~/.claude/agents/hooks/inject_worktree_env
    
  2. 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

  1. Claude Code calls the script before executing a Bash tool
  2. Script checks if POLYWAVE_AGENT_WORKTREE environment variable is set (injected by Hook 10)
  3. If unset or empty -> pass through (exit 0, no modification)
  4. If command already starts with cd $POLYWAVE_AGENT_WORKTREE -> pass through (no double-injection)
  5. Otherwise -> return updatedInput with command modified to cd $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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/inject_bash_cd ~/.claude/agents/hooks/inject_bash_cd
    
  2. 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

  1. Claude Code calls the script before executing Write/Edit tools
  2. Script checks if POLYWAVE_AGENT_WORKTREE environment variable is set
  3. If unset or empty -> pass through (solo wave agents use different isolation)
  4. If file_path is relative (doesn't start with /) -> block (exit 2) with error message
  5. If file_path doesn't start with $POLYWAVE_AGENT_WORKTREE -> block (exit 2) with boundary violation message
  6. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_write_paths ~/.claude/agents/hooks/validate_write_paths
    
  2. 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

  1. Claude Code calls the script when a subagent stops (after last tool execution)
  2. Script checks if POLYWAVE_AGENT_ID and POLYWAVE_IMPL_PATH environment variables are set
  3. If unset -> pass through (non-Polywave agent)
  4. Reads IMPL doc and extracts completion report for the agent
  5. If completion report missing -> warn to stderr (exit 0, non-blocking)
  6. If POLYWAVE_BRANCH is set, checks that the branch has at least one commit
  7. If no commits found -> warn to stderr (exit 0, non-blocking)
  8. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/verify_worktree_compliance ~/.claude/agents/hooks/verify_worktree_compliance
    
  2. 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

  1. Claude Code calls the script when a subagent stops (after last tool execution)
  2. Script extracts agent metadata (type, ID, wave number, IMPL path) from environment and description
  3. Emits a structured JSON event to stdout with completion timestamp, agent context, and session metadata
  4. Runs asynchronously and non-blocking — always exits 0 regardless of success/failure
  5. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/emit_agent_completion ~/.claude/agents/hooks/emit_agent_completion
    
  2. 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

  1. Claude Code calls the script when the session is about to stop
  2. Script checks stop_hook_active field — if true, exits 0 immediately (loop prevention)
  3. Scans docs/IMPL/IMPL-*.yaml for any IMPL with state: WAVE_PENDING or state: WAVE_EXECUTING
  4. Checks if .claude/worktrees/polywave/ contains any active worktree directories
  5. If either check finds active work -> emit systemMessage warning (non-blocking, exit 0)
  6. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/polywave_orchestrator_stop ~/.claude/agents/hooks/polywave_orchestrator_stop
    
  2. 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

  1. Claude Code calls the script when any subagent stops
  2. Script checks if agent description contains [polywave:scout tag; non-scouts pass through
  3. Locates the IMPL doc (via .polywave-state/active-impl, description path extraction, or filesystem scan)
  4. Runs polywave-tools validate --fix (auto-corrects fixable issues: invalid gate types, unknown keys)
  5. Runs polywave-tools validate again (read-only) to check if errors remain
  6. If errors persist after fix -> block (exit 2) with detailed error output
  7. 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

  1. Symlink:

    ln -sf ~/code/polywave/implementations/claude-code/hooks/validate_scout_output ~/.claude/agents/hooks/validate_scout_output
    
  2. 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 pull updates 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 additionalContext on stdout
  • Execution: Runs synchronously (blocks tool execution if it exits non-zero)
  • Idempotent: Running install.sh multiple times is safe (updates existing symlinks)