Skip to content
Agent Orchestration
Hook

Hooks

What pi runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
pi
1074 skills4 agents3 commands7 hooks
Install
> /plugin marketplace add share-skills/pi
> /plugin install pi@pi-skills

Ships with pi. 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.

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/capture-prompt.sh

PreCompact

  • Matches*bash ${CLAUDE_PLUGIN_ROOT}/hooks/pre-compact.sh

Stop

  • Matches*bash ${CLAUDE_PLUGIN_ROOT}/hooks/capture-decision.sh

PostToolUse

  • MatchesBashbash ${CLAUDE_PLUGIN_ROOT}/hooks/capture-tool-result.sh success

PostToolUseFailure

  • MatchesBashbash ${CLAUDE_PLUGIN_ROOT}/hooks/capture-tool-result.sh failure

SubagentStart

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/capture-subagent.sh start

SubagentStop

  • bash ${CLAUDE_PLUGIN_ROOT}/hooks/capture-subagent.sh stop
Read hooks/hooks.json

In the plugin's words

How pi describes its own hook set.

PI 智行合一引擎 hooks — 上下文恢复协议 + 决策可视化数据采集

Where it lives

  • hooks/capture-decision.shRunsGitHub
    Read the script
    #!/bin/bash
    #
    # Licensed to the Apache Software Foundation (ASF) under one or more
    # contributor license agreements.  See the NOTICE file distributed with
    # this work for additional information regarding copyright ownership.
    # The ASF licenses this file to You under the Apache License, Version 2.0
    # (the "License"); you may not use this file except in compliance with
    # the License.  You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    # Copyright (c) 2026 HePin
    #
    
    # PI Decision Capture Hook
    # 触发时机:Session Stop 或 关键决策点
    # 功能:捕获当前上下文状态、决策元数据,生成结构化日志
    
    # Load shared utilities
    # 假设脚本位于 hooks 目录下,或者 CLAUDE_PLUGIN_ROOT 已设置
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if [ -f "${SCRIPT_DIR}/pi-shared.sh" ]; then
        source "${SCRIPT_DIR}/pi-shared.sh"
    else
        # Fallback if shared script missing
        echo "Error: pi-shared.sh not found in ${SCRIPT_DIR}" >&2
        exit 1
    fi
    
    HOOK_INPUT=$(cat)
    INPUT_SESSION_ID=$(json_extract_string_field "$HOOK_INPUT" "session_id")
    if [ -n "$INPUT_SESSION_ID" ]; then
        export PI_SESSION_ID="$INPUT_SESSION_ID"
    fi
    
    # --- Metadata Collection ---
    
    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    NODE_ID=$(generate_node_id)
    SESSION_ID=$(get_session_id)
    
    # Try to capture git context (sanitized before persisting to JSONL).
    GIT_BRANCH=$(summarize_text "$(git branch --show-current 2>/dev/null || echo "unknown")" 120)
    GIT_COMMIT=$(summarize_text "$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")" 60)
    
    # Try to capture recent PI status from environment or logs
    # 这里假设 PI 运行时会将状态写入临时文件,或者我们可以从环境变量推断
    # 如果没有,使用默认值
    SCENE="${PI_SCENE:-unknown}"
    BATTLE_LEVEL="${PI_BATTLE_LEVEL:-1}"
    FAILURE_COUNT="${PI_FAILURE_COUNT:-0}"
    BEAST="${PI_BEAST:-none}"
    STRATEGY="${PI_STRATEGY:-default}"
    SKILL_NAME="${PI_SKILL_NAME:-pi}"
    SKILL_VERSION="${PI_SKILL_VERSION:-20}"
    COGNITIVE_MODE="${PI_COGNITIVE_MODE:-}"
    INTERACTION_MODE="${PI_INTERACTION_MODE:-}"
    CONFIDENCE="${PI_CONFIDENCE:-}"
    CLASSICAL_REFERENCE=$(summarize_text "${PI_CLASSICAL_REFERENCE:-}" 120)
    THOUGHT_CHAIN=$(summarize_text "${PI_THOUGHT_CHAIN:-}" 200)
    SCENE_ANNOUNCEMENT=$(summarize_text "${PI_SCENE_ANNOUNCEMENT:-}" 160)
    STATE_SIGNAL=$(summarize_text "${PI_STATE_SIGNAL:-}" 160)
    DELIVERY_CONTRACT=$(summarize_text "${PI_DELIVERY_CONTRACT:-}" 160)
    HUMAN_INPUT_SUMMARY=$(summarize_text "${PI_HUMAN_INPUT_SUMMARY:-}" 160)
    ASSISTANT_QUESTION=$(summarize_text "${PI_ASSISTANT_QUESTION:-}" 160)
    USER_DECISION=$(summarize_text "${PI_USER_DECISION:-}" 120)
    RESONANCE_FORMS="${PI_RESONANCE_FORMS:-}"
    
    # Decision Category inference
    # 如果是 Stop hook,通常意味着 delivery 或 intercept
    CATEGORY="delivery" 
    if [ "$FAILURE_COUNT" -gt 0 ]; then
        CATEGORY="battle"
    fi
    
    BATTLE_LEVEL_JSON=$(json_number_or_zero "$BATTLE_LEVEL")
    FAILURE_COUNT_JSON=$(json_number_or_zero "$FAILURE_COUNT")
    DIFFICULTY_JSON=$(difficulty_for_level "$BATTLE_LEVEL_JSON")
    MODEL_NAME=$(current_model_name)
    MODEL_PROVIDER=$(current_model_provider)
    MODEL_INPUT_TOKENS=$(current_input_tokens)
    MODEL_OUTPUT_TOKENS=$(current_output_tokens)
    MODEL_CACHED_INPUT_TOKENS=$(current_cached_input_tokens)
    MODEL_REASONING_TOKENS=$(current_reasoning_tokens)
    MODEL_TOOL_TOKENS=$(current_tool_tokens)
    MODEL_ESTIMATED_COST=$(current_estimated_cost_usd)
    
    if [ -z "$THOUGHT_CHAIN" ]; then
        THOUGHT_CHAIN=$(summarize_text "场景 ${SCENE} · 难度 ${DIFFICULTY_JSON} · 战势 ${BATTLE_LEVEL_JSON} · 策略 ${STRATEGY} · 灵兽 ${BEAST}" 200)
    fi
    if [ -z "$SCENE_ANNOUNCEMENT" ]; then
        SCENE_ANNOUNCEMENT=$(summarize_text "🧠 PI · ${SCENE} · ${DIFFICULTY_JSON}" 120)
    fi
    if [ -z "$RESONANCE_FORMS" ]; then
        RESONANCE_FORMS="transparent_chain"
        if [ -n "$STATE_SIGNAL" ]; then
            RESONANCE_FORMS="${RESONANCE_FORMS},state_signal"
        fi
        if [ -n "$DELIVERY_CONTRACT" ] || [ -n "$ASSISTANT_QUESTION" ]; then
            RESONANCE_FORMS="${RESONANCE_FORMS},delivery_contract"
        fi
    fi
    
    JSON_PAYLOAD="{\"node_id\": $(json_escape "$NODE_ID"), \"session_id\": $(json_escape "$SESSION_ID"), \"timestamp\": $(json_escape "$TIMESTAMP"), \"label\": $(json_escape "$SCENE_ANNOUNCEMENT"), \"category\": $(json_escape "$CATEGORY"), \"decision_point\": \"session.stop\", \"scene\": $(json_escape "$SCENE"), \"difficulty\": $(json_escape "$DIFFICULTY_JSON"), \"battle_level\": ${BATTLE_LEVEL_JSON}, \"failure_count\": ${FAILURE_COUNT_JSON}, \"agent_id\": \"pi\", \"payload\": {\"beast\": $(json_escape "$BEAST"), \"strategy\": $(json_escape "$STRATEGY"), \"git_context\": {\"branch\": $(json_escape "$GIT_BRANCH"), \"commit\": $(json_escape "$GIT_COMMIT")}, \"pi_context\": {\"skill_name\": $(json_escape "$SKILL_NAME"), \"skill_version\": $(json_escape "$SKILL_VERSION"), \"scene_announcement\": $(json_escape "$SCENE_ANNOUNCEMENT"), \"thought_chain_summary\": $(json_escape "$THOUGHT_CHAIN"), \"cognitive_mode\": $(json_escape "$COGNITIVE_MODE"), \"interaction_mode\": $(json_escape "$INTERACTION_MODE"), \"confidence\": $(json_escape "$CONFIDENCE"), \"classical_reference\": $(json_escape "$CLASSICAL_REFERENCE"), \"state_signal\": $(json_escape "$STATE_SIGNAL"), \"delivery_contract\": $(json_escape "$DELIVERY_CONTRACT"), \"resonance_forms\": $(json_escape "$RESONANCE_FORMS")}, \"human_context\": {\"input_summary\": $(json_escape "$HUMAN_INPUT_SUMMARY"), \"assistant_question\": $(json_escape "$ASSISTANT_QUESTION"), \"user_decision\": $(json_escape "$USER_DECISION")}, \"model_context\": {\"name\": $(json_escape "$MODEL_NAME"), \"provider\": $(json_escape "$MODEL_PROVIDER"), \"input_tokens\": $(json_number_or_zero "$MODEL_INPUT_TOKENS"), \"output_tokens\": $(json_number_or_zero "$MODEL_OUTPUT_TOKENS"), \"cached_input_tokens\": $(json_number_or_zero "$MODEL_CACHED_INPUT_TOKENS"), \"reasoning_tokens\": $(json_number_o
  • hooks/capture-generic.shGitHub
    Read the script
    #!/bin/bash
    #
    # Licensed to the Apache Software Foundation (ASF) under one or more
    # contributor license agreements.  See the NOTICE file distributed with
    # this work for additional information regarding copyright ownership.
    # The ASF licenses this file to You under the Apache License, Version 2.0
    # (the "License"); you may not use this file except in compliance with
    # the License.  You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    # Copyright (c) 2026 HePin
    #
    
    # PI Generic Capture Hook
    # Captures arbitrary JSON events from external tools (Qoder, standalone scripts, etc.)
    # Usage: echo '{"event": "build_success", "details": "..."}' | ./capture-generic.sh
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if [ -f "${SCRIPT_DIR}/pi-shared.sh" ]; then
        source "${SCRIPT_DIR}/pi-shared.sh"
    else
        echo "Error: pi-shared.sh not found" >&2
        exit 1
    fi
    
    # Read input
    HOOK_INPUT=$(cat)
    
    # Extract or generate session/node IDs
    INPUT_SESSION_ID=$(json_extract_string_field "$HOOK_INPUT" "session_id")
    if [ -n "$INPUT_SESSION_ID" ]; then
        export PI_SESSION_ID="$INPUT_SESSION_ID"
    fi
    
    SESSION_ID=$(get_session_id)
    NODE_ID=$(generate_node_id)
    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    
    # Ensure session file exists
    if [ ! -f "${SESSION_DIR}/session-${SESSION_ID}.json" ]; then
        init_session_file
    fi
    
    # --- Metadata Extraction ---
    # Try to extract key fields if the input is JSON, otherwise use defaults.
    LABEL=$(json_extract_string_field "$HOOK_INPUT" "label")
    if [ -z "$LABEL" ]; then
        # Privacy boundary: never derive labels from arbitrary raw input (may contain secrets).
        LABEL="Generic Event"
    fi
    
    CATEGORY=$(json_extract_string_field "$HOOK_INPUT" "category")
    if [ -z "$CATEGORY" ]; then CATEGORY="external"; fi
    
    DECISION_POINT=$(json_extract_string_field "$HOOK_INPUT" "decision_point")
    if [ -z "$DECISION_POINT" ]; then DECISION_POINT="event.capture"; fi
    
    SCENE=$(json_extract_string_field "$HOOK_INPUT" "scene")
    if [ -z "$SCENE" ]; then SCENE="external"; fi
    
    OUTCOME=$(json_extract_string_field "$HOOK_INPUT" "outcome")
    if [ -z "$OUTCOME" ]; then OUTCOME="captured"; fi
    
    # We must escape the input carefully for JSON string compatibility.
    # The shared helper json_escape handles quotes, newlines, tabs, backslashes.
    
    # Sanitize input for privacy safety
    SANITIZED_INPUT=$(sanitize_text "$HOOK_INPUT")
    ESCAPED_INPUT=$(json_escape "$SANITIZED_INPUT")
    
    # Manually construct JSON Node
    # This structure mimics a decision node so the visualizer can render it as part of the graph.
    # We embed the original (sanitized) input as the payload.
    
    JSON_PAYLOAD="{\"node_id\": $(json_escape "$NODE_ID"), \"session_id\": $(json_escape "$SESSION_ID"), \"timestamp\": $(json_escape "$TIMESTAMP"), \"label\": $(json_escape "$LABEL"), \"category\": $(json_escape "$CATEGORY"), \"decision_point\": $(json_escape "$DECISION_POINT"), \"scene\": $(json_escape "$SCENE"), \"difficulty\": \"⚡\", \"battle_level\": 0, \"failure_count\": 0, \"agent_id\": \"external\", \"payload\": $ESCAPED_INPUT, \"privacy_level\": \"redacted\", \"outcome\": $(json_escape "$OUTCOME"), \"children_node_ids\": []}"
    
    # Write to JSONL file (using append_decision_node to treat it as a graph node)
    append_decision_node "$JSON_PAYLOAD"
    
    # Logging for debugging
    log_debug " captured generic event: $NODE_ID ($LABEL)"
    echo "Captured generic event: $NODE_ID"
    
  • hooks/capture-prompt.shRunsGitHub
    Read the script
    #!/bin/bash
    #
    # Licensed to the Apache Software Foundation (ASF) under one or more
    # contributor license agreements.  See the NOTICE file distributed with
    # this work for additional information regarding copyright ownership.
    # The ASF licenses this file to You under the Apache License, Version 2.0
    # (the "License"); you may not use this file except in compliance with
    # the License.  You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    # Copyright (c) 2026 HePin
    #
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if [ -f "${SCRIPT_DIR}/pi-shared.sh" ]; then
        source "${SCRIPT_DIR}/pi-shared.sh"
    else
        echo "Error: pi-shared.sh not found in ${SCRIPT_DIR}" >&2
        exit 1
    fi
    
    HOOK_INPUT=$(cat)
    INPUT_SESSION_ID=$(json_extract_string_field "$HOOK_INPUT" "session_id")
    if [ -n "$INPUT_SESSION_ID" ]; then
        export PI_SESSION_ID="$INPUT_SESSION_ID"
    fi
    
    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    NODE_ID=$(generate_node_id)
    SESSION_ID=$(get_session_id)
    
    PROMPT_TEXT=$(json_extract_string_field "$HOOK_INPUT" "prompt")
    PROMPT_SUMMARY=$(summarize_text "$PROMPT_TEXT" 180)
    PROMPT_WORDS=$(word_count "$PROMPT_TEXT")
    SCENE="${PI_SCENE:-user_interaction}"
    BATTLE_LEVEL="${PI_BATTLE_LEVEL:-1}"
    DIFFICULTY=$(difficulty_for_level "$BATTLE_LEVEL")
    FAILURE_COUNT="${PI_FAILURE_COUNT:-0}"
    INTERACTION_MODE="${PI_INTERACTION_MODE:-}"
    
    if [ -z "$PROMPT_SUMMARY" ]; then
        PROMPT_SUMMARY="Human input"
    fi
    
    JSON_PAYLOAD="{\"node_id\": $(json_escape "$NODE_ID"), \"session_id\": $(json_escape "$SESSION_ID"), \"timestamp\": $(json_escape "$TIMESTAMP"), \"label\": $(json_escape "$PROMPT_SUMMARY"), \"category\": \"interaction\", \"decision_point\": \"human.input\", \"scene\": $(json_escape "$SCENE"), \"difficulty\": $(json_escape "$DIFFICULTY"), \"battle_level\": $(json_number_or_zero "$BATTLE_LEVEL"), \"failure_count\": $(json_number_or_zero "$FAILURE_COUNT"), \"retry_count\": $(json_number_or_zero "$FAILURE_COUNT"), \"agent_id\": \"human\", \"payload\": {\"prompt_summary\": $(json_escape "$PROMPT_SUMMARY"), \"word_count\": $(json_number_or_zero "$PROMPT_WORDS"), \"interaction_point\": \"user_prompt_submit\", \"scene_snapshot\": $(json_escape "$SCENE"), \"interaction_mode\": $(json_escape "$INTERACTION_MODE")}, \"privacy_level\": \"redacted\", \"outcome\": \"pending\", \"children_node_ids\": []}"
    
    append_event_jsonl "$JSON_PAYLOAD"
    log_debug " captured prompt input: $NODE_ID"
    
  • hooks/capture-subagent.shRunsGitHub
    Read the script
    #!/bin/bash
    #
    # Licensed to the Apache Software Foundation (ASF) under one or more
    # contributor license agreements.  See the NOTICE file distributed with
    # this work for additional information regarding copyright ownership.
    # The ASF licenses this file to You under the Apache License, Version 2.0
    # (the "License"); you may not use this file except in compliance with
    # the License.  You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    # Copyright (c) 2026 HePin
    #
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if [ -f "${SCRIPT_DIR}/pi-shared.sh" ]; then
        source "${SCRIPT_DIR}/pi-shared.sh"
    else
        echo "Error: pi-shared.sh not found in ${SCRIPT_DIR}" >&2
        exit 1
    fi
    
    EVENT_KIND="${1:-start}"
    HOOK_INPUT=$(cat)
    INPUT_SESSION_ID=$(json_extract_string_field "$HOOK_INPUT" "session_id")
    if [ -n "$INPUT_SESSION_ID" ]; then
        export PI_SESSION_ID="$INPUT_SESSION_ID"
    fi
    
    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    NODE_ID=$(generate_node_id)
    SESSION_ID=$(get_session_id)
    
    AGENT_ID=$(json_extract_string_field "$HOOK_INPUT" "agent_id")
    AGENT_TYPE=$(json_extract_string_field "$HOOK_INPUT" "agent_type")
    PARENT_AGENT_ID=$(json_extract_string_field "$HOOK_INPUT" "parent_agent_id")
    DISPLAY_NAME=$(json_extract_string_field "$HOOK_INPUT" "display_name")
    AGENT_ROLE=$(infer_role_from_agent_type "$AGENT_TYPE")
    SCENE="${PI_SCENE:-team_collaboration}"
    BATTLE_LEVEL="${PI_BATTLE_LEVEL:-1}"
    DIFFICULTY=$(difficulty_for_level "$BATTLE_LEVEL")
    OUTCOME="pending"
    
    # Infer stop outcome from hook input so failed subagents surface as failures in the UI.
    infer_subagent_stop_outcome() {
        local hook_input="$1"
    
        local raw outcome status success_str
        outcome=$(json_extract_string_field "$hook_input" "outcome")
        status=$(json_extract_string_field "$hook_input" "status")
        success_str=$(json_extract_string_field "$hook_input" "success")
    
        normalize_word() {
            printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[[:space:]]+/_/g'
        }
    
        raw=$(normalize_word "$outcome")
        case "$raw" in
            success|succeeded|ok|passed|complete|completed) printf 'success'; return 0 ;;
            failure|failed|error|errored) printf 'failure'; return 0 ;;
            cancel|cancelled|canceled|aborted) printf 'cancelled'; return 0 ;;
        esac
    
        raw=$(normalize_word "$status")
        case "$raw" in
            success|succeeded|ok|passed|complete|completed) printf 'success'; return 0 ;;
            failure|failed|error|errored) printf 'failure'; return 0 ;;
            cancel|cancelled|canceled|aborted) printf 'cancelled'; return 0 ;;
        esac
    
        raw=$(normalize_word "$success_str")
        case "$raw" in
            true|1|yes) printf 'success'; return 0 ;;
            false|0|no) printf 'failure'; return 0 ;;
        esac
    
        if printf '%s' "$hook_input" | grep -Eq '"success"[[:space:]]*:[[:space:]]*false'; then
            printf 'failure'
            return 0
        fi
        if printf '%s' "$hook_input" | grep -Eq '"success"[[:space:]]*:[[:space:]]*true'; then
            printf 'success'
            return 0
        fi
    
        extract_int_field() {
            local field="$1"
            printf '%s' "$hook_input" | sed -nE 's/.*"'"$field"'"[[:space:]]*:[[:space:]]*"?(-?[0-9]+)"?.*/\1/p; q'
        }
    
        local exit_code
        exit_code=$(extract_int_field "exit_code")
        if [ -z "$exit_code" ]; then
            exit_code=$(extract_int_field "exitCode")
        fi
        if [ -n "$exit_code" ] && [ "$exit_code" != "0" ]; then
            printf 'failure'
            return 0
        fi
    
        local error_message
        error_message=$(json_extract_string_field "$hook_input" "error")
        if [ -n "$error_message" ]; then
            printf 'failure'
            return 0
        fi
    
        if printf '%s' "$hook_input" | grep -Eq '"error"[[:space:]]*:[[:space:]]*(\{|\[)'; then
            printf 'failure'
            return 0
        fi
    
        printf 'success'
    }
    
    if [ "$EVENT_KIND" = "stop" ]; then
        OUTCOME=$(infer_subagent_stop_outcome "$HOOK_INPUT")
    fi
    
    if [ -z "$AGENT_TYPE" ]; then
        AGENT_TYPE="subagent"
    fi
    
    LABEL=$(summarize_text "Subagent ${EVENT_KIND}: ${AGENT_TYPE}" 120)
    
    JSON_PAYLOAD="{\"node_id\": $(json_escape "$NODE_ID"), \"session_id\": $(json_escape "$SESSION_ID"), \"timestamp\": $(json_escape "$TIMESTAMP"), \"label\": $(json_escape "$LABEL"), \"category\": \"team\", \"decision_point\": $(json_escape "subagent.${EVENT_KIND}"), \"scene\": $(json_escape "$SCENE"), \"difficulty\": $(json_escape "$DIFFICULTY"), \"battle_level\": $(json_number_or_zero "$BATTLE_LEVEL"), \"agent_id\": $(json_escape "${AGENT_ID:-$AGENT_TYPE}"), \"payload\": {\"agent_id\": $(json_escape "$AGENT_ID"), \"agent_type\": $(json_escape "$AGENT_TYPE"), \"parent_agent_id\": $(json_escape "$PARENT_AGENT_ID"), \"display_name\": $(json_escape "$DISPLAY_NAME"), \"agent_role\": $(json_escape "$AGENT_ROLE"), \"identity\": $(json_escape "$AGENT_TYPE"), \"event\": $(json_escape "$EVENT_KIND")}, \"privacy_level\": \"redacted\", \"outcome\": $(json_escape "$OUTCOME"), \"children_node_ids\": []}"
    
    append_event_jsonl "$JSON_PAYLOAD"
    log_debug " captured subagent ${EVENT_KIND}: ${AGENT_TYPE}"
    
  • hooks/capture-tool-result.shRunsGitHub
    Read the script
    #!/bin/bash
    #
    # Licensed to the Apache Software Foundation (ASF) under one or more
    # contributor license agreements.  See the NOTICE file distributed with
    # this work for additional information regarding copyright ownership.
    # The ASF licenses this file to You under the Apache License, Version 2.0
    # (the "License"); you may not use this file except in compliance with
    # the License.  You may obtain a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    # Copyright (c) 2026 HePin
    #
    
    # PI Tool Result Capture Hook
    # 触发时机:PostToolUse (Bash 命令执行后)
    # 功能:捕获工具执行结果、耗时、状态码,用于分析执行效率和失败模式
    
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if [ -f "${SCRIPT_DIR}/pi-shared.sh" ]; then
        source "${SCRIPT_DIR}/pi-shared.sh"
    else
        echo "Error: pi-shared.sh not found in ${SCRIPT_DIR}" >&2
        exit 1
    fi
    
    HOOK_VARIANT="${1:-success}"
    HOOK_INPUT=$(cat)
    INPUT_SESSION_ID=$(json_extract_string_field "$HOOK_INPUT" "session_id")
    if [ -n "$INPUT_SESSION_ID" ]; then
        export PI_SESSION_ID="$INPUT_SESSION_ID"
    fi
    
    # --- Metadata Collection ---
    
    TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    NODE_ID=$(generate_node_id)
    SESSION_ID=$(get_session_id)
    
    TOOL_NAME="${COPILOT_TOOL_NAME:-$(json_extract_string_field "$HOOK_INPUT" "tool_name")}"
    if [ -z "$TOOL_NAME" ]; then
        TOOL_NAME="bash"
    fi
    EXIT_CODE="${COPILOT_TOOL_EXIT_CODE:-}"
    if [ -z "$EXIT_CODE" ]; then
        if [ "$HOOK_VARIANT" = "failure" ]; then
            EXIT_CODE="1"
        else
            EXIT_CODE="0"
        fi
    fi
    DURATION_MS="${COPILOT_TOOL_DURATION_MS:-0}"
    EXIT_CODE=$(json_number_or_zero "$EXIT_CODE")
    DURATION_MS=$(json_number_or_zero "$DURATION_MS")
    OUTPUT_SUMMARY="${COPILOT_TOOL_OUTPUT_SUMMARY:-}"
    if [ -n "$OUTPUT_SUMMARY" ]; then
        OUTPUT_SUMMARY=$(summarize_text "$OUTPUT_SUMMARY" 180)
    fi
    
    # Fallback: if no specific tool info, optionally try to get last command from history.
    # Disabled by default because shell history may contain sensitive arguments.
    if [ "$TOOL_NAME" == "bash" ] && [ -z "$OUTPUT_SUMMARY" ] && [ "${PI_CAPTURE_HISTORY_FALLBACK:-0}" = "1" ]; then
        # Try to read last command from history file if available
        # Note: This is best-effort and might not work in all environments
        LAST_CMD=$(history 1 | sed 's/^[ ]*[0-9]*[ ]*//')
        if [ -n "$LAST_CMD" ]; then
            # Sanitize sensitive info from command line
            SANITIZED_CMD=$(sanitize_text "$LAST_CMD")
            OUTPUT_SUMMARY="Command executed: ${SANITIZED_CMD:0:100}..."
        fi
    fi
    
    # Determine outcome based on exit code
    OUTCOME="success"
    if [ "$EXIT_CODE" -ne 0 ]; then
        OUTCOME="failure"
    fi
    
    MODEL_NAME=$(current_model_name)
    MODEL_PROVIDER=$(current_model_provider)
    
    JSON_PAYLOAD="{\"node_id\": $(json_escape "$NODE_ID"), \"session_id\": $(json_escape "$SESSION_ID"), \"timestamp\": $(json_escape "$TIMESTAMP"), \"label\": $(json_escape "$(summarize_text "${TOOL_NAME} ${OUTCOME}" 96)"), \"category\": \"exec\", \"decision_point\": \"tool.execution\", \"scene\": \"execution\", \"difficulty\": \"⚡\", \"battle_level\": 1, \"failure_count\": $( [ \"$OUTCOME\" = \"failure\" ] && printf '1' || printf '0' ), \"payload\": {\"tool\": $(json_escape "$TOOL_NAME"), \"exit_code\": ${EXIT_CODE}, \"duration_ms\": ${DURATION_MS}, \"summary\": $(json_escape "$OUTPUT_SUMMARY"), \"hook_event\": $(json_escape "$HOOK_VARIANT"), \"model_context\": {\"name\": $(json_escape "$MODEL_NAME"), \"provider\": $(json_escape "$MODEL_PROVIDER"), \"input_tokens\": $(json_number_or_zero "$(current_input_tokens)"), \"output_tokens\": $(json_number_or_zero "$(current_output_tokens)")}}, \"privacy_level\": \"redacted\", \"outcome\": $(json_escape "$OUTCOME"), \"children_node_ids\": []}"
    
    # Write to JSONL file
    append_event_jsonl "$JSON_PAYLOAD"
    
    # Logging
    log_debug " captured tool result: $TOOL_NAME (exit: $EXIT_CODE)"
    
  • hooks/pi-shared.shGitHub
  • hooks/pre-compact.shRunsGitHub
    Read the script
    #!/bin/bash
    # PI 智行合一引擎 — PreCompact Hook
    # 在上下文压缩前注入恢复协议提醒,确保 PI 状态被保存
    #
    # 对应 PI §8.9 上下文恢复协议:
    #   会话续接或压缩后:🔍查势(失败计数+战阶)→ 🎯查域(场景+认知阵)→ 📝查簿(已试策略簿)
    
    cat << 'EOF'
    {
      "systemMessage": "⚠️ PI 上下文恢复协议(PreCompact 自动触发):上下文即将压缩。请在压缩前输出恢复快照,格式:\n\n🔄 PI · 恢复 · {当前场景} · 败{N} · {当前战阶} · 已排{M}策\n\n包含:当前场景、失败计数、战阶、已试策略簿、关键决策、当前任务状态。压缩后将基于此快照恢复 PI 引擎状态。"
    }
    EOF
    

All 7 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.

Ships withpi

阿里内网用户欢迎加群交流 【群号: 156800004200 / TMCP 天使用户群】 PI(智行合一) 是一套融汇东方百家智慧、禅宗截教修行哲学、MBTI 认知策略与西方方法论的 AI 引擎,驱动人机协同达到前所未有的高度。它不是在"惩罚" AI——而是唤醒 AI 内在的卓越本能,让 AI 与人共同开创文明新纪元。

Get the whole plugin