Hooks
What humanize runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add PolyArch/humanize > /plugin install humanize@PolyArch
Ships with humanize. Installing the plugin gets these hooks.
What fires, and when
UserPromptSubmit
Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.
${CLAUDE_PLUGIN_ROOT}/hooks/loop-plan-file-validator.sh
PreToolUse
- Matches
Write${CLAUDE_PLUGIN_ROOT}/hooks/loop-write-validator.sh - Matches
Edit${CLAUDE_PLUGIN_ROOT}/hooks/loop-edit-validator.sh - Matches
Read${CLAUDE_PLUGIN_ROOT}/hooks/loop-read-validator.sh - Matches
Bash${CLAUDE_PLUGIN_ROOT}/hooks/loop-bash-validator.sh
PostToolUse
- Matches
Bash${CLAUDE_PLUGIN_ROOT}/hooks/loop-post-bash-hook.sh
Stop
${CLAUDE_PLUGIN_ROOT}/hooks/loop-codex-stop-hook.sh
In the plugin's words
How humanize describes its own hook set.
Humanize Plugin Hooks - Validation hooks and Stop hooks for /start-rlcr-loop
Where it lives
- hooks/check-todos-from-transcript.pyGitHub
- hooks/loop-bash-validator.shRunsGitHub
Read the script
#!/usr/bin/env bash # # PreToolUse Hook: Validate Bash commands for RLCR loop # # Blocks attempts to bypass Write/Edit hooks using shell commands: # - cat/echo/printf > file.md (redirection) # - tee file.md # - sed -i file.md (in-place edit) # - goal-tracker.md modifications via Bash # set -euo pipefail # Load shared functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" source "$SCRIPT_DIR/lib/loop-common.sh" # ======================================== # Parse Hook Input # ======================================== HOOK_INPUT=$(cat) # Validate JSON input structure if ! validate_hook_input "$HOOK_INPUT"; then exit 1 fi # Check for deeply nested JSON (potential DoS) if is_deeply_nested "$HOOK_INPUT" 30; then exit 1 fi TOOL_NAME="$VALIDATED_TOOL_NAME" if [[ "$TOOL_NAME" != "Bash" ]]; then exit 0 fi # Require command for Bash tool if ! require_tool_input_field "$HOOK_INPUT" "command"; then exit 1 fi COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // ""') COMMAND_LOWER=$(to_lower "$COMMAND") # ======================================== # Find Active Loops (needed for multiple checks) # ======================================== PROJECT_ROOT="$(resolve_project_root)" || exit 0 # Extract session_id from hook input for session-aware loop filtering HOOK_SESSION_ID=$(extract_session_id "$HOOK_INPUT") # Check for active RLCR loop (filtered by session_id) LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr" ACTIVE_LOOP_DIR=$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID") # ======================================== # Methodology Analysis Phase Bash Restriction # ======================================== # During methodology analysis, block file-modifying bash commands. # Only read-only operations and cancel-rlcr-loop.sh are allowed. # This prevents source code modifications after Codex has signed off. # # Accepted limitations: # - Read-only bash commands (cat, grep, find, etc.) are NOT blocked. Blocking # them would break basic Claude operations. The analysis prompt directs Claude # to derive user-facing content only from methodology-analysis-report.md. # - Spawned agents (different session_id) are not restricted by hooks; their # sanitization is enforced by the analysis prompt. This is an inherent # limitation of the hook architecture which cannot distinguish spawned agents # from unrelated sessions. # # Use only the session-matched loop. Do NOT fall back to an unfiltered search, # as that would incorrectly restrict unrelated sessions opened in the same repo. _MA_BASH_DIR="$ACTIVE_LOOP_DIR" if [[ -n "$_MA_BASH_DIR" ]] && [[ -f "$_MA_BASH_DIR/methodology-analysis-state.md" ]]; then # Allow cancel-rlcr-loop.sh only as the leading command (not as an argument # to another command like cp/mv). The optional path prefix must be a single # token with no embedded whitespace, otherwise commands like # `bash cancel-rlcr-loop.sh` or `tee cancel-rlcr-loop.sh` would match. # The script name must be followed by whitespace or end-of-line so trailing # tokens cannot hide additional arguments. # # Also reject any shell metacharacter that can inject or redirect work # after the cancel invocation: pipes/sequence/background operators, # command substitution ($(...) or backticks), redirection (<, >), and # multi-line payloads. The earlier narrower check only rejected ; | &, # letting payloads like `cancel-rlcr-loop.sh $(touch /tmp/pwn)` or a # newline-delimited second command slip past this early exit and reach # arbitrary file modifications before the downstream blockers run. _ma_has_shell_meta=false case "$COMMAND_LOWER" in *';'*|*'|'*|*'&'*|*'`'*|*'>'*|*'<'*|*'$('*|*$'\n'*) _ma_has_shell_meta=true ;; esac if [[ "$_ma_has_shell_meta" != "true" ]] && \ echo "$COMMAND_LOWER" | grep -qE '^[[:space:]]*"?([^[:space:]"]+/)?cancel-rlcr-loop\.sh"?([[:space:]]|$)'; then exit 0 fi # Block git commands that modify the working tree if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])git[[:space:]]+(commit|add|reset|checkout|merge|rebase|cherry-pick|am|apply|stash|push|restore|clean|rm|mv|switch|pull|clone|submodule|worktree)'; then echo "# Bash Blocked During Methodology Analysis Git write commands are not allowed during the methodology analysis phase." >&2 exit 2 fi # Block file manipulation commands (touch, mv, cp, rm, mkdir, ln, patch, etc.) if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(tee|install|touch|mv|cp|rm|dd|truncate|chmod|chown|mkdir|rmdir|ln|mktemp|patch)[[:space:]]'; then echo "# Bash Blocked During Methodology Analysis File modification commands are not allowed during the methodology analysis phase." >&2 exit 2 fi # Block in-place file editing tools if echo "$COMMAND_LOWER" | grep -qE 'sed[[:space:]]+-i|awk[[:space:]]+-i[[:space:]]+inplace|perl[[:space:]]+-[^[:space:]]*i'; then echo "# Bash Blocked During Methodology Analysis In-place file editing is not allowed during the methodology analysis phase." >&2 exit 2 fi # Block common interpreters that could write files (defense-in-depth) if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(python[23]?|ruby|node|perl|php)[[:space:]]'; then echo "# Bash Blocked During Methodology Analysis Running interpreters is not allowed during the methodology analysis phase." >&2 exit 2 fi # Block shell script entry points (bash script.sh, sh script.sh, source, .) if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&])(/usr/bin/env[[:space:]]+)?(bash|sh|zsh|/bin/bash|/bin/sh|/bin/zsh)[[:space:]]'; then echo "# Bash Blocked During Methodology Analysis Running shell scripts is not allowed during the methodology analysis phase." >&2 exit 2 fi # Block build tools that execute arbitrary commands if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space - hooks/loop-codex-stop-hook.shRunsGitHub
Read the script
#!/usr/bin/env bash # # Stop Hook for RLCR loop # # Intercepts Claude's exit attempts and uses Codex to review work. # If Codex doesn't confirm completion, blocks exit and feeds review back. # # State directory: .humanize/rlcr/<timestamp>/ # State file: state.md (current_round, max_iterations, codex config) # Summary file: round-N-summary.md (Claude's work summary) # Review prompt: round-N-review-prompt.md (prompt sent to Codex) # Review result: round-N-review-result.md (Codex's review) # set -euo pipefail # ======================================== # Default Configuration # ======================================== # DEFAULT_CODEX_MODEL and DEFAULT_CODEX_EFFORT are provided by loop-common.sh (sourced below) DEFAULT_CODEX_TIMEOUT=5400 # ======================================== # Read Hook Input # ======================================== HOOK_INPUT=$(cat) # NOTE: We intentionally do NOT check stop_hook_active here. # For iterative loops, stop_hook_active will be true when Claude is continuing # from a previous blocked stop. We WANT to run Codex review each iteration. # Loop termination is controlled by: # - No active loop directory (no state.md) -> exit early below # - Codex outputs MARKER_COMPLETE -> allow exit # - current_round >= max_iterations -> allow exit # ======================================== # Find Active Loop # ======================================== # Source shared loop functions and template loader SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" source "$SCRIPT_DIR/lib/loop-common.sh" PROJECT_ROOT="$(resolve_project_root)" || exit 0 LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr" # Source portable timeout wrapper for git operations PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" source "$PLUGIN_ROOT/scripts/portable-timeout.sh" # Source methodology analysis library source "$SCRIPT_DIR/lib/methodology-analysis.sh" # Default timeout for git operations (30 seconds) GIT_TIMEOUT=30 # Template directory is set by loop-common.sh via template-loader.sh # Extract session_id from hook input for session-aware loop filtering HOOK_SESSION_ID=$(extract_session_id "$HOOK_INPUT") LOOP_DIR=$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID" true) # If no active loop (or session_id mismatch), allow exit if [[ -z "$LOOP_DIR" ]]; then exit 0 fi # ======================================== # Background-Task Guards # ======================================== # Delegates to handle_bg_task_short_circuit (hooks/lib/loop-bg-tasks.sh), # which runs four cohesive guards in order: # 1. Ambiguous-caller marker guard (no session_id + marker present) # 2. Cross-session parked-loop guard (foreign session walking in) # 3. Pending-bg short-circuit (this session has async work in flight) # 4. Same-session stale-marker cleanup (bg work just finished) # When any guard short-circuits, it emits the appropriate JSON on stdout # and `exit 0`s directly; we never return from that call. When no guard # fires we continue into the normal gate logic below. handle_bg_task_short_circuit "$LOOP_DIR" "$HOOK_INPUT" "$HOOK_SESSION_ID" # ======================================== # Detect Loop Phase: Normal or Finalize # ======================================== # Normal loop: state.md exists # Finalize Phase: finalize-state.md exists (after Codex COMPLETE, before final completion) STATE_FILE=$(resolve_active_state_file "$LOOP_DIR") if [[ -z "$STATE_FILE" ]]; then # No state file found, allow exit exit 0 fi IS_FINALIZE_PHASE=false [[ "$STATE_FILE" == *"/finalize-state.md" ]] && IS_FINALIZE_PHASE=true IS_METHODOLOGY_ANALYSIS_PHASE=false [[ "$STATE_FILE" == *"/methodology-analysis-state.md" ]] && IS_METHODOLOGY_ANALYSIS_PHASE=true # ======================================== # Parse State File (using shared function) # ======================================== # First extract raw frontmatter to check which fields are actually present # This prevents silently using defaults for missing critical fields RAW_FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE" 2>/dev/null || echo "") # Check if critical fields are present before parsing (which applies defaults) RAW_CURRENT_ROUND=$(echo "$RAW_FRONTMATTER" | grep "^current_round:" || true) RAW_MAX_ITERATIONS=$(echo "$RAW_FRONTMATTER" | grep "^max_iterations:" || true) RAW_FULL_REVIEW_ROUND=$(echo "$RAW_FRONTMATTER" | grep "^full_review_round:" || true) RAW_BITLESSON_REQUIRED=$(echo "$RAW_FRONTMATTER" | grep "^bitlesson_required:" || true) RAW_BITLESSON_FILE=$(echo "$RAW_FRONTMATTER" | grep "^bitlesson_file:" || true) RAW_BITLESSON_ALLOW_EMPTY_NONE=$(echo "$RAW_FRONTMATTER" | grep "^bitlesson_allow_empty_none:" || true) # Use tolerant parsing to extract values # Note: parse_state_file applies defaults for missing current_round/max_iterations if ! parse_state_file "$STATE_FILE" 2>/dev/null; then echo "Warning: parse_state_file returned non-zero, proceeding to schema validation" >&2 fi # Map STATE_* variables to local names for backward compatibility PLAN_TRACKED="$STATE_PLAN_TRACKED" START_BRANCH="$STATE_START_BRANCH" BASE_BRANCH="${STATE_BASE_BRANCH:-}" BASE_COMMIT="${STATE_BASE_COMMIT:-}" PLAN_FILE="$STATE_PLAN_FILE" CURRENT_ROUND="$STATE_CURRENT_ROUND" MAX_ITERATIONS="$STATE_MAX_ITERATIONS" PUSH_EVERY_ROUND="$STATE_PUSH_EVERY_ROUND" FULL_REVIEW_ROUND="${STATE_FULL_REVIEW_ROUND:-5}" REVIEW_STARTED="$STATE_REVIEW_STARTED" CODEX_EXEC_MODEL="${STATE_CODEX_MODEL:-$DEFAULT_CODEX_MODEL}" CODEX_EXEC_EFFORT="${STATE_CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}" CODEX_REVIEW_MODEL="$CODEX_EXEC_MODEL" CODEX_REVIEW_EFFORT="high" CODEX_TIMEOUT="${STATE_CODEX_TIMEOUT:-${CODEX_TIMEOUT:-$DEFAULT_CODEX_TIMEOUT}}" ASK_CODEX_QUESTION="${STATE_ASK_CODEX_QUESTION:-false}" AGENT_TEAMS="${STATE_AGENT_TEAMS:-false}" PRIVACY_MODE="${STATE_PRIVACY_MODE:-true}" BITLESSON_REQUIRED="false" if [[ -n "$RAW_BITLESSON_REQUIRED" ]]; then BITLESSON_REQUIRED=$(echo "$RAW_BITLESSON_REQUIRED" | sed 's/^bitlesson_required:[[:space:]]*//' | tr -d ' "') fi BITLES - hooks/loop-edit-validator.shRunsGitHub
Read the script
#!/usr/bin/env bash # # PreToolUse Hook: Validate Edit paths for RLCR loop # # Blocks Claude from editing: # - Todos files (should use native Task tools instead) # - Prompt files (read-only, generated by Codex) # - State files (managed by hooks, not Claude) # - Wrong round number contract files # - Goal tracker edits outside the active loop or that alter the immutable section # set -euo pipefail # Load shared functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" source "$SCRIPT_DIR/lib/loop-common.sh" # ======================================== # Parse Hook Input # ======================================== HOOK_INPUT=$(cat) TOOL_NAME=$(echo "$HOOK_INPUT" | jq -r '.tool_name // ""') if [[ "$TOOL_NAME" != "Edit" ]]; then exit 0 fi FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""') FILE_PATH_LOWER=$(to_lower "$FILE_PATH") # Extract session_id from hook input for session-aware loop filtering HOOK_SESSION_ID=$(extract_session_id "$HOOK_INPUT") # ======================================== # Block Todos and Prompt Files # ======================================== if is_round_file_type "$FILE_PATH_LOWER" "todos"; then PROJECT_ROOT="$(resolve_project_root)" || exit 0 LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr" LOOP_DIR=$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID") if [[ -z "$LOOP_DIR" ]] || ! is_allowlisted_file "$FILE_PATH" "$LOOP_DIR"; then todos_blocked_message "Edit" >&2 exit 2 fi fi if is_round_file_type "$FILE_PATH_LOWER" "prompt"; then prompt_write_blocked_message >&2 exit 2 fi # ======================================== # Methodology Analysis Phase Edit Restriction # ======================================== # During methodology analysis, only methodology artifacts can be edited. # This prevents source code modifications after Codex has signed off. # This check MUST come before the humanize loop dir early exit below. PROJECT_ROOT="${PROJECT_ROOT:-$(resolve_project_root 2>/dev/null || true)}" [[ -z "$PROJECT_ROOT" ]] && exit 0 LOOP_BASE_DIR="${LOOP_BASE_DIR:-$PROJECT_ROOT/.humanize/rlcr}" # Use only the session-matched loop. Do NOT fall back to an unfiltered search, # as that would incorrectly restrict unrelated sessions opened in the same repo. # Limitation: Spawned agents (different session_id) are not restricted by hooks; # their sanitization is enforced by the analysis prompt. _MA_LOOP_DIR="${LOOP_DIR:-$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID")}" if [[ -n "$_MA_LOOP_DIR" ]] && [[ -f "$_MA_LOOP_DIR/methodology-analysis-state.md" ]]; then # If realpath fails (file doesn't exist yet on BSD/macOS), resolve parent dir _ma_real_path=$(realpath "$FILE_PATH" 2>/dev/null || echo "") if [[ -z "$_ma_real_path" ]]; then _ma_parent=$(realpath "$(dirname "$FILE_PATH")" 2>/dev/null || echo "") [[ -n "$_ma_parent" ]] && _ma_real_path="$_ma_parent/$(basename "$FILE_PATH")" fi _ma_real_loop=$(realpath "$_MA_LOOP_DIR" 2>/dev/null || echo "") # Fallback to raw paths when realpath is unavailable (older macOS/BSD) # Ensure paths are absolute and reject ".." to prevent traversal bypasses. if [[ -z "$_ma_real_path" ]]; then if [[ "$FILE_PATH" == *".."* ]]; then echo "# Edit Blocked During Methodology Analysis Path contains traversal segments that cannot be resolved without realpath." >&2 exit 2 fi # Fail closed if the leaf is a symlink we cannot resolve; the raw # path would satisfy the loop-dir prefix check while pointing at a # target outside the loop, letting the basename allowlist approve # edits to arbitrary files during methodology-analysis mode. if [[ -L "$FILE_PATH" ]]; then echo "# Edit Blocked During Methodology Analysis Path is a symlink that cannot be resolved without realpath." >&2 exit 2 fi if [[ "$FILE_PATH" == /* ]]; then _ma_real_path="$FILE_PATH" else _ma_real_path="$PROJECT_ROOT/$FILE_PATH" fi fi if [[ -z "$_ma_real_loop" ]]; then if [[ "$_MA_LOOP_DIR" == /* ]]; then _ma_real_loop="$_MA_LOOP_DIR" else _ma_real_loop="$PROJECT_ROOT/$_MA_LOOP_DIR" fi fi if [[ "$_ma_real_path" == "$_ma_real_loop/"* ]]; then _ma_basename=$(basename "$_ma_real_path") case "$_ma_basename" in methodology-analysis-report.md|methodology-analysis-done.md) exit 0 ;; esac fi echo "# Edit Blocked During Methodology Analysis During the methodology analysis phase, only methodology artifacts can be edited. Allowed: methodology-analysis-report.md, methodology-analysis-done.md" >&2 exit 2 fi # ======================================== # Check if File is in .humanize/rlcr # ======================================== if ! is_in_humanize_loop_dir "$FILE_PATH"; then exit 0 fi # ======================================== # Find Active Loop and Current Round # ======================================== PROJECT_ROOT="${PROJECT_ROOT:-$(resolve_project_root 2>/dev/null || true)}" [[ -z "$PROJECT_ROOT" ]] && exit 0 LOOP_BASE_DIR="${LOOP_BASE_DIR:-$PROJECT_ROOT/.humanize/rlcr}" ACTIVE_LOOP_DIR="${LOOP_DIR:-$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID")}" if [[ -z "$ACTIVE_LOOP_DIR" ]]; then exit 0 fi # Detect if we're in Finalize Phase (finalize-state.md exists) STATE_FILE_TO_PARSE=$(resolve_active_state_file "$ACTIVE_LOOP_DIR") IS_FINALIZE_PHASE=false if [[ "$STATE_FILE_TO_PARSE" == *"/finalize-state.md" ]]; then IS_FINALIZE_PHASE=true fi # Parse state file using strict validation (fail closed on malformed state) if ! parse_state_file_strict "$STATE_FILE_TO_PARSE" 2>/dev/null; then echo "Error: Malformed state file, blocking operation for safety" >&2 exit 1 fi CURRENT_ROUND="$STATE_CURRENT_ROUND" # ======================================== # Blo - hooks/loop-plan-file-validator.shRunsGitHub
Read the script
#!/usr/bin/env bash # # UserPromptSubmit hook for plan file validation during RLCR loop # # Validates: # - State schema version (plan_tracked, start_branch fields required) # - Branch consistency (no switching during loop) # - Plan file tracking status consistency # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" # Source shared loop functions and template loader source "$SCRIPT_DIR/lib/loop-common.sh" PROJECT_ROOT="$(resolve_project_root)" || exit 0 # Source portable timeout wrapper for git operations PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" source "$PLUGIN_ROOT/scripts/portable-timeout.sh" # Default timeout for git operations (30 seconds) GIT_TIMEOUT=30 # Read hook input (required for UserPromptSubmit hooks) INPUT=$(cat) # Extract session_id from hook input for session-aware loop filtering HOOK_SESSION_ID=$(extract_session_id "$INPUT") # Find active loop using shared function (filtered by session_id) LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr" LOOP_DIR=$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID") # If no active loop, allow exit if [[ -z "$LOOP_DIR" ]]; then exit 0 fi # Detect if we're in Finalize Phase (finalize-state.md exists) STATE_FILE=$(resolve_active_state_file "$LOOP_DIR") # Parse state file using strict validation (fail closed on malformed state) if ! parse_state_file_strict "$STATE_FILE" 2>/dev/null; then echo "Error: Malformed state file, blocking operation for safety" >&2 exit 1 fi # Map STATE_* variables to local names for backward compatibility PLAN_TRACKED="$STATE_PLAN_TRACKED" PLAN_FILE="$STATE_PLAN_FILE" START_BRANCH="$STATE_START_BRANCH" # ======================================== # Schema Validation (v1.1.2+ required fields) # ======================================== # Helper function to output schema validation error schema_validation_error() { local field_name="$1" local fallback="RLCR loop state file is missing required field: \`${field_name}\`\n\nThis indicates the loop was started with an older version of humanize.\n\n**Options:**\n1. Cancel the loop: \`/humanize:cancel-rlcr-loop\`\n2. Update humanize plugin to version 1.1.2+\n3. Restart the RLCR loop with the updated plugin" local reason reason=$(load_and_render_safe "$TEMPLATE_DIR" "block/schema-outdated.md" "$fallback" "FIELD_NAME=$field_name") # Escape newlines for JSON local escaped_reason escaped_reason=$(echo "$reason" | jq -Rs '.') cat << EOF { "decision": "block", "reason": $escaped_reason } EOF } # Check required fields (using FIELD_* constants from loop-common.sh) REQUIRED_FIELDS=("${FIELD_PLAN_TRACKED}:$PLAN_TRACKED" "${FIELD_START_BRANCH}:$START_BRANCH") for field_entry in "${REQUIRED_FIELDS[@]}"; do field_name="${field_entry%%:*}" field_value="${field_entry#*:}" if [[ -z "$field_value" ]]; then schema_validation_error "$field_name" exit 0 fi done # ======================================== # Branch Consistency Check # ======================================== # Use || GIT_EXIT_CODE=$? to prevent set -e from aborting on non-zero exit CURRENT_BRANCH=$(run_with_timeout "$GIT_TIMEOUT" git -C "$PROJECT_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null) || GIT_EXIT_CODE=$? GIT_EXIT_CODE=${GIT_EXIT_CODE:-0} if [[ $GIT_EXIT_CODE -ne 0 || -z "$CURRENT_BRANCH" ]]; then cat << EOF { "decision": "block", "reason": "Git operation failed or timed out.\\n\\nCannot verify branch consistency. Please check git status and try again." } EOF exit 0 fi if [[ -n "$START_BRANCH" && "$CURRENT_BRANCH" != "$START_BRANCH" ]]; then cat << EOF { "decision": "block", "reason": "Git branch has changed during RLCR loop.\\n\\nStarted on: $START_BRANCH\\nCurrent: $CURRENT_BRANCH\\n\\nBranch switching is not allowed during an active RLCR loop. Please switch back to the original branch or cancel the loop with /humanize:cancel-rlcr-loop" } EOF exit 0 fi # ======================================== # Plan File Tracking Status Check # ======================================== FULL_PLAN_PATH="$PROJECT_ROOT/$PLAN_FILE" if [[ "$PLAN_TRACKED" == "true" ]]; then # Must be tracked and clean # Use || LS_FILES_EXIT=$? to prevent set -e from aborting on non-zero exit # ls-files --error-unmatch returns: 0 (tracked), 1 (not tracked), 124 (timeout), other (error) run_with_timeout "$GIT_TIMEOUT" git -C "$PROJECT_ROOT" ls-files --error-unmatch "$PLAN_FILE" &>/dev/null || LS_FILES_EXIT=$? LS_FILES_EXIT=${LS_FILES_EXIT:-0} if [[ $LS_FILES_EXIT -eq 124 ]]; then # Timeout - fail closed cat << EOF { "decision": "block", "reason": "Git operation timed out while checking plan file tracking status.\\n\\nPlease check git status and try again." } EOF exit 0 elif [[ $LS_FILES_EXIT -ne 0 && $LS_FILES_EXIT -ne 1 ]]; then # Unexpected git error - fail closed cat << EOF { "decision": "block", "reason": "Git operation failed while checking plan file tracking status (exit code: $LS_FILES_EXIT).\\n\\nPlease check git status and try again." } EOF exit 0 fi PLAN_IS_TRACKED=$([[ $LS_FILES_EXIT -eq 0 ]] && echo "true" || echo "false") # Use || STATUS_EXIT=$? to prevent set -e from aborting on non-zero exit # git status --porcelain returns: 0 (success), 124 (timeout), other (error) PLAN_GIT_STATUS=$(run_with_timeout "$GIT_TIMEOUT" git -C "$PROJECT_ROOT" status --porcelain "$PLAN_FILE" 2>/dev/null) || STATUS_EXIT=$? STATUS_EXIT=${STATUS_EXIT:-0} if [[ $STATUS_EXIT -eq 124 ]]; then # Timeout - fail closed cat << EOF { "decision": "block", "reason": "Git operation timed out while checking plan file status.\\n\\nPlease check git status and try again." } EOF exit 0 elif [[ $STATUS_EXIT -ne 0 ]]; then # Unexpected git error - fail closed cat << EOF { "decision": "block", "reason": "Git operation failed while checking plan file status (exit code: $STATUS_E - hooks/loop-post-bash-hook.shRunsGitHub
Read the script
#!/usr/bin/env bash # # PostToolUse Bash Hook for RLCR loop # # Records the Claude Code session_id into state.md immediately after setup. # This hook fires right after the setup script's Bash command completes. # # Mechanism: # 1. Setup script creates .humanize/.pending-session-id with: # Line 1: path to state.md # Line 2: full resolved path of setup script (command signature) # 2. This hook checks for the signal file on every Bash PostToolUse event # 3. Boundary-aware match: verifies the Bash command is a valid invocation # of the setup script path (path followed by end-of-string or whitespace), # preventing false positives from substrings and concatenated forms # 4. Extracts session_id from hook JSON input # 5. Patches state.md with the session_id value using safe awk replacement # 6. Removes the signal file (one-shot mechanism) # # This ensures session_id is recorded BEFORE any team members can be created, # so only the team leader (main session) is affected by RLCR loop hooks. # set -euo pipefail # Read hook JSON input from stdin HOOK_INPUT=$(cat) # Determine project root using the shared deterministic resolver. # If neither CLAUDE_PROJECT_DIR nor a git toplevel is available, there # is no active loop to patch - exit cleanly (pwd is NOT used as a # fallback because it drifts with `cd` during a session). SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" source "$SCRIPT_DIR/lib/project-root.sh" PROJECT_ROOT="$(resolve_project_root)" || exit 0 # Check for pending session_id signal file SIGNAL_FILE="$PROJECT_ROOT/.humanize/.pending-session-id" if [[ ! -f "$SIGNAL_FILE" ]]; then # No pending session_id to record - this is the normal case exit 0 fi # Read the signal file contents # Line 1: state file path # Line 2: full resolved path of setup script (command signature) STATE_FILE_PATH="" COMMAND_SIGNATURE="" { read -r STATE_FILE_PATH || true read -r COMMAND_SIGNATURE || true } < "$SIGNAL_FILE" if [[ -z "$STATE_FILE_PATH" ]] || [[ ! -f "$STATE_FILE_PATH" ]]; then # Signal file is empty or points to non-existent state file - clean up rm -f "$SIGNAL_FILE" exit 0 fi # Verify the Bash command is a real setup script invocation (not arbitrary text) # The command signature is the full resolved path of setup-rlcr-loop.sh. # We require the command to START with this path (quoted or unquoted), # preventing false positives like 'echo setup-rlcr-loop.sh' from consuming the signal. if [[ -n "$COMMAND_SIGNATURE" ]]; then HOOK_COMMAND="" if command -v jq >/dev/null 2>&1; then HOOK_COMMAND=$(printf '%s' "$HOOK_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "") fi if [[ -z "$HOOK_COMMAND" ]]; then exit 0 fi # Normalize consecutive slashes (e.g. "PolyArch//scripts" -> "PolyArch/scripts"). # CLAUDE_PLUGIN_ROOT may have a trailing slash, producing double slashes when # concatenated with "/scripts/..." in the command template. The setup script # normalizes its own path via cd+pwd (removing double slashes), but the # tool_input.command preserves the original string. Without normalization, # the string comparison below always fails and session_id is never written. # See: https://github.com/PolyArch/humanize/issues/67 HOOK_COMMAND=$(printf '%s' "$HOOK_COMMAND" | tr -s '/') COMMAND_SIGNATURE=$(printf '%s' "$COMMAND_SIGNATURE" | tr -s '/') # Boundary-aware match: command must be a valid setup invocation form. # Requires the script path to be followed by end-of-string or any POSIX # whitespace ([[:space:]]), preventing concatenated forms. # Accepts: "/full/path/setup-rlcr-loop.sh" args (quoted, space-delimited) # "/full/path/setup-rlcr-loop.sh"\targs (quoted, tab-delimited) # "/full/path/setup-rlcr-loop.sh" (quoted, no args) # /full/path/setup-rlcr-loop.sh args (unquoted, space-delimited) # /full/path/setup-rlcr-loop.sh\targs (unquoted, tab-delimited) # /full/path/setup-rlcr-loop.sh (unquoted, no args) # Rejects: "/full/path/setup-rlcr-loop.sh"foo (no boundary after quote) # echo /full/path/setup-rlcr-loop.sh (does not start with path) IS_SETUP="false" if [[ "$HOOK_COMMAND" == "\"${COMMAND_SIGNATURE}\"" ]] || [[ "$HOOK_COMMAND" == "\"${COMMAND_SIGNATURE}\""[[:space:]]* ]]; then IS_SETUP="true" elif [[ "$HOOK_COMMAND" == "${COMMAND_SIGNATURE}" ]] || [[ "$HOOK_COMMAND" == "${COMMAND_SIGNATURE}"[[:space:]]* ]]; then IS_SETUP="true" fi if [[ "$IS_SETUP" != "true" ]]; then # This Bash event is not from the setup script - do not consume signal exit 0 fi fi # Extract session_id from the hook JSON input SESSION_ID="" if command -v jq >/dev/null 2>&1; then SESSION_ID=$(printf '%s' "$HOOK_INPUT" | jq -r '.session_id // empty' 2>/dev/null || echo "") fi if [[ -z "$SESSION_ID" ]]; then # No session_id available in hook input - leave signal file for next attempt exit 0 fi # Patch state.md: replace empty session_id with actual value # Only patch if session_id is currently empty (safety check) CURRENT_SESSION_ID=$(grep "^session_id:" "$STATE_FILE_PATH" 2>/dev/null | sed 's/session_id: *//' || echo "") if [[ -z "$CURRENT_SESSION_ID" ]]; then # Use awk for safe replacement (handles special chars in SESSION_ID: /, &, etc.) TEMP_FILE="${STATE_FILE_PATH}.tmp.$$" awk -v new_id="$SESSION_ID" '{ if ($0 ~ /^session_id:$/) { print "session_id: " new_id } else { print } }' "$STATE_FILE_PATH" > "$TEMP_FILE" mv "$TEMP_FILE" "$STATE_FILE_PATH" fi # Remove signal file (one-shot: session_id is now recorded) rm -f "$SIGNAL_FILE" exit 0 - hooks/loop-read-validator.shRunsGitHub
Read the script
#!/usr/bin/env bash # # PreToolUse Hook: Validate Read access for RLCR loop files # # Blocks Claude from reading: # - Wrong round's prompt/summary/contract files (outdated information) # - Round files from wrong locations (not in .humanize/rlcr/) # - Round files from old session directories # - Todos files (should use native Task tools instead) # - goal-tracker.md from old RLCR sessions # set -euo pipefail # Load shared functions SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" source "$SCRIPT_DIR/lib/loop-common.sh" # ======================================== # Parse Hook Input # ======================================== HOOK_INPUT=$(cat) # Validate JSON input structure if ! validate_hook_input "$HOOK_INPUT"; then exit 1 fi # Check for deeply nested JSON (potential DoS) if is_deeply_nested "$HOOK_INPUT" 30; then exit 1 fi TOOL_NAME="$VALIDATED_TOOL_NAME" if [[ "$TOOL_NAME" != "Read" ]]; then exit 0 fi # Require file_path for Read tool if ! require_tool_input_field "$HOOK_INPUT" "file_path"; then exit 1 fi FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // ""') FILE_PATH_LOWER=$(to_lower "$FILE_PATH") # Extract session_id from hook input for session-aware loop filtering HOOK_SESSION_ID=$(extract_session_id "$HOOK_INPUT") # ======================================== # Block Todos Files # ======================================== if is_round_file_type "$FILE_PATH_LOWER" "todos"; then PROJECT_ROOT="$(resolve_project_root)" || exit 0 LOOP_BASE_DIR="$PROJECT_ROOT/.humanize/rlcr" LOOP_DIR=$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID") if [[ -z "$LOOP_DIR" ]] || ! is_allowlisted_file "$FILE_PATH" "$LOOP_DIR"; then todos_blocked_message "Read" >&2 exit 2 fi fi # ======================================== # Methodology Analysis Phase Read Restriction # ======================================== # During methodology analysis, restrict reads of files within the loop # directory to only the artifacts the analysis agent needs. This prevents # project-specific information from leaking into the analysis report. # Files outside the loop directory are allowed (Claude needs system files). # This check MUST come before the summary/prompt early exit below, # otherwise non-summary/prompt files in the loop dir escape restriction. PROJECT_ROOT="${PROJECT_ROOT:-$(resolve_project_root 2>/dev/null || true)}" [[ -z "$PROJECT_ROOT" ]] && exit 0 LOOP_BASE_DIR="${LOOP_BASE_DIR:-$PROJECT_ROOT/.humanize/rlcr}" # Use only the session-matched loop. Do NOT fall back to an unfiltered search, # as that would incorrectly restrict unrelated sessions opened in the same repo. # Limitation: Spawned agents (different session_id) are not restricted by hooks; # their sanitization is enforced by the analysis prompt. ACTIVE_LOOP_DIR="${LOOP_DIR:-$(find_active_loop "$LOOP_BASE_DIR" "$HOOK_SESSION_ID")}" _MA_CHECK_DIR="$ACTIVE_LOOP_DIR" if [[ -n "$_MA_CHECK_DIR" ]]; then _MA_STATE=$(resolve_active_state_file "$_MA_CHECK_DIR") if [[ "$_MA_STATE" == *"/methodology-analysis-state.md" ]]; then # Canonicalize to prevent path traversal # If realpath fails (file doesn't exist yet on BSD/macOS), resolve parent dir _ma_real_path=$(realpath "$FILE_PATH" 2>/dev/null || echo "") if [[ -z "$_ma_real_path" ]]; then _ma_parent=$(realpath "$(dirname "$FILE_PATH")" 2>/dev/null || echo "") [[ -n "$_ma_parent" ]] && _ma_real_path="$_ma_parent/$(basename "$FILE_PATH")" fi _ma_real_loop=$(realpath "$_MA_CHECK_DIR" 2>/dev/null || echo "") # Fallback to raw paths when realpath is unavailable (older macOS/BSD) # Ensure paths are absolute so prefix guards cannot be bypassed. # Reject paths with ".." segments to prevent traversal bypasses # when we cannot canonicalize (fail closed). if [[ -z "$_ma_real_path" ]]; then if [[ "$FILE_PATH" == *".."* ]]; then echo "# Read Blocked During Methodology Analysis Path contains traversal segments that cannot be resolved without realpath." >&2 exit 2 fi # Fail closed if the file is a symlink we cannot resolve; the raw # path would skip the project-root prefix guard, allowing a symlink # outside the project to point back at restricted project content. if [[ -L "$FILE_PATH" ]]; then echo "# Read Blocked During Methodology Analysis Path is a symlink that cannot be resolved without realpath." >&2 exit 2 fi if [[ "$FILE_PATH" == /* ]]; then _ma_real_path="$FILE_PATH" else _ma_real_path="$PROJECT_ROOT/$FILE_PATH" fi fi if [[ -z "$_ma_real_loop" ]]; then if [[ "$_MA_CHECK_DIR" == /* ]]; then _ma_real_loop="$_MA_CHECK_DIR" else _ma_real_loop="$PROJECT_ROOT/$_MA_CHECK_DIR" fi fi if [[ "$_ma_real_path" == "$_ma_real_loop/"* ]]; then _ma_basename=$(basename "$_ma_real_path") # Allowlist: only methodology artifacts (not raw development records). # Raw records (round-*-summary.md, round-*-review-result.md) are # intentionally excluded so the originating session cannot read # project-specific content and must rely solely on the sanitized # methodology-analysis-report.md for all user-facing output. # The spawned Opus agent reads raw records directly (not restricted # by hooks due to different session_id -- see limitation comment above). case "$_ma_basename" in methodology-analysis-report.md|methodology-analysis-done.md|methodology-analysis-state.md) exit 0 ;; *) echo "# Read Blocked During Methodology Analysis Only me - hooks/loop-write-validator.shRunsGitHub
All 8 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Derived from the GAAC (GitHub-as-a-Context) project. A Claude Code plugin that provides iterative development with independent AI review. Build with confidence through continuous feedback loops.

