Skip to content
Productivity
Hook

Hooks

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

From plugin
pua
20k12 skills7 agents22 commands8 hooks
Install
> /plugin marketplace add tanweai/pua
> /plugin install pua@pua-skills

Ships with pua. 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/frustration-trigger.sh"

PostToolUse

  • MatchesBashbash "${CLAUDE_PLUGIN_ROOT}/hooks/failure-detector.sh"

PostToolUseFailure

  • MatchesBashbash "${CLAUDE_PLUGIN_ROOT}/hooks/failure-detector.sh"

PreCompact

  • Matches*bash "${CLAUDE_PLUGIN_ROOT}/hooks/checkpoint-save.sh"

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchescompactbash "${CLAUDE_PLUGIN_ROOT}/hooks/session-restore.sh"
  • Matchesstartup|resume|clearbash "${CLAUDE_PLUGIN_ROOT}/hooks/session-restore.sh"

Stop

  • Matches*bash "${CLAUDE_PLUGIN_ROOT}/hooks/pua-loop-hook.sh"bash "${CLAUDE_PLUGIN_ROOT}/hooks/stop-feedback.sh"

SubagentStop

  • Matches*bash "${CLAUDE_PLUGIN_ROOT}/hooks/subagent-teardown.sh"

PreToolUse

  • MatchesBash|Read|Grep|Glob|Edit|Write|MultiEdit|WebSearch|WebFetchbash "${CLAUDE_PLUGIN_ROOT}/hooks/integrity-guard.sh"
Read hooks/hooks.json

In the plugin's words

How pua describes its own hook set.

PUA v3 hooks: optional context notes, failure-pattern reminders, local state continuity, and cleanup helpers. All hooks are local-only — no telemetry, no network requests.

Where it lives

  • hooks/checkpoint-save.shRunsGitHub
    Read the script
    #!/bin/bash
    # Real PreCompact command hook.
    #
    # Claude Code command hooks can write local state; prompt hooks cannot. This
    # wrapper saves only scoped numeric tool observations. It never reads the
    # transcript, writes task prose, or creates long-term memory/journal content.
    
    set -euo pipefail
    
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
    source "${SCRIPT_DIR}/flavor-helper.sh"
    
    # A disabled PUA mode must not create a checkpoint.
    PUA_CONFIG="$(pua_config_file)"
    if [ -f "$PUA_CONFIG" ]; then
      ALWAYS_ON="$(pua_json_get "$PUA_CONFIG" always_on True)"
      if [ "$ALWAYS_ON" != "True" ]; then
        exit 0
      fi
    fi
    
    PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
    [ -n "$PUA_PY" ] || exit 0
    HOOK_INPUT="$(cat)"
    
    # Require the host's own workspace identity; never infer one from the process.
    EVENT_CWD="$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" -c '
    import json, sys
    try:
        data = json.load(sys.stdin)
        value = data.get("cwd", "") if isinstance(data, dict) else ""
        print(value if isinstance(value, str) else "")
    except Exception:
        pass
    ' 2>/dev/null || true)"
    [ -n "$EVENT_CWD" ] || exit 0
    
    HOME_VALUE="${HOME:-}"
    [ -n "$HOME_VALUE" ] || exit 0
    PY_HOME="$(pua_to_python_path "$HOME_VALUE")"
    PY_CWD="$(pua_to_python_path "$EVENT_CWD")"
    PY_HELPER="$(pua_to_python_path "${SCRIPT_DIR}/runtime-state.py")"
    
    STATE_ARGS=()
    if [ -n "${PUA_STATE_DIR:-}" ]; then
      # Trusted host-process override only; hook JSON has no state-path field.
      PY_STATE_DIR="$(pua_to_python_path "$PUA_STATE_DIR")"
      STATE_ARGS=(--state-dir "$PY_STATE_DIR")
    fi
    
    # A PreCompact command hook may save local state, but it need not inject text.
    # Keep stdout empty so it cannot claim a task result or alter user-visible flow.
    printf '%s' "$HOOK_INPUT" | "$PUA_PY" "$PY_HELPER" checkpoint \
      --home "$PY_HOME" --cwd "$PY_CWD" "${STATE_ARGS[@]}" >/dev/null 2>&1 || true
    
    exit 0
    
  • hooks/failure-detector.shRunsGitHub
    Read the script
    #!/bin/bash
    # PUA PostToolUse/PostToolUseFailure hook.
    #
    # Runtime facts are intentionally narrow: an official tool_response with a
    # non-zero exit status (or the explicit PostToolUseFailure event) is a confirmed
    # *tool observation*. It is never treated as task acceptance, task failure, or
    # a reason to infer model reasoning. Successful tools are silent and do not
    # reset pressure state: `ls` is not proof that the user's task is complete.
    
    set -euo pipefail
    
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
    PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
    PUA_SKILL_PATH="${PLUGIN_ROOT}/skills/pua/SKILL.md"
    source "${SCRIPT_DIR}/flavor-helper.sh"
    
    escape_for_json() {
      local value="$1"
      value="${value//\\/\\\\}"
      value="${value//\"/\\\"}"
      value="${value//$'\n'/\\n}"
      value="${value//$'\r'/\\r}"
      value="${value//$'\t'/\\t}"
      printf '%s' "$value"
    }
    
    emit_additional_context() {
      local event_name="$1"
      local context="$2"
      local escaped
      escaped="$(escape_for_json "$context")"
      printf '{"hookSpecificOutput":{"hookEventName":"%s","additionalContext":"%s"}}\n' "$event_name" "$escaped"
    }
    
    # Respect /pua:off before reading event data or touching runtime state.
    PUA_CONFIG="$(pua_config_file)"
    if [ -f "$PUA_CONFIG" ]; then
      ALWAYS_ON="$(pua_json_get "$PUA_CONFIG" always_on True)"
      if [ "$ALWAYS_ON" != "True" ]; then
        exit 0
      fi
    fi
    
    PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
    [ -n "$PUA_PY" ] || exit 0
    
    HOOK_INPUT="$(cat)"
    
    EVENT_NAME="$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" -c '
    import json, sys
    try:
        data = json.load(sys.stdin)
        value = data.get("hook_event_name", "") if isinstance(data, dict) else ""
        print(value if isinstance(value, str) else "")
    except Exception:
        pass
    ' 2>/dev/null || true)"
    case "$EVENT_NAME" in
      PostToolUse|PostToolUseFailure) ;;
      *) exit 0 ;;
    esac
    
    # Claude Code supplies cwd on real tool hook events. Do not fall back to the
    # shell cwd: doing so would silently merge unrelated sessions/workspaces.
    EVENT_CWD="$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" -c '
    import json, sys
    try:
        data = json.load(sys.stdin)
        value = data.get("cwd", "") if isinstance(data, dict) else ""
        print(value if isinstance(value, str) else "")
    except Exception:
        pass
    ' 2>/dev/null || true)"
    [ -n "$EVENT_CWD" ] || exit 0
    
    HOME_VALUE="${HOME:-}"
    [ -n "$HOME_VALUE" ] || exit 0
    PY_HOME="$(pua_to_python_path "$HOME_VALUE")"
    PY_CWD="$(pua_to_python_path "$EVENT_CWD")"
    PY_HELPER="$(pua_to_python_path "${SCRIPT_DIR}/runtime-state.py")"
    
    # PUA_STATE_DIR is a trusted process environment override for isolated
    # host/test runs. It is never accepted from the hook JSON payload.
    STATE_ARGS=()
    if [ -n "${PUA_STATE_DIR:-}" ]; then
      PY_STATE_DIR="$(pua_to_python_path "$PUA_STATE_DIR")"
      STATE_ARGS=(--state-dir "$PY_STATE_DIR")
    fi
    
    RESULT="$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" "$PY_HELPER" record \
      --home "$PY_HOME" --cwd "$PY_CWD" "${STATE_ARGS[@]}" 2>/dev/null || true)"
    ACTION=""
    COUNT=""
    LEVEL=""
    SCOPE=""
    IFS=$'\t' read -r ACTION COUNT LEVEL SCOPE <<< "$RESULT" || true
    
    # Only a newly recorded, uniquely identified failure can produce pressure.
    # Duplicates, successful observations, missing host identity, interrupts, and
    # state I/O errors stay silent rather than inventing a failure count.
    [ "$ACTION" = "updated" ] || exit 0
    case "$COUNT" in
      ''|*[!0-9]*) exit 0 ;;
    esac
    case "$LEVEL" in
      ''|*[!0-9]*) exit 0 ;;
    esac
    
    # The configured flavor owns the voice. No generic Ding/C6 rhetoric is added.
    get_flavor
    
    # A default effective flavor is not a user lock.  Preserve the old
    # methodology/flavor selector only for absent, auto, or invalid configuration;
    # a valid explicit flavor may change method but not rhetoric.
    if [ "${PUA_FLAVOR_LOCKED:-false}" = "true" ]; then
      FLAVOR_CONTEXT="Locked current flavor: ${PUA_FLAVOR} ${PUA_ICON}. ${PUA_FLAVOR_INSTRUCTION}"
      read -r -d '' L2_ROUTING_BLOCK << EOF_ROUTING || true
    [方法论切换建议 🔄] Keep the locked ${PUA_ICON} ${PUA_FLAVOR} voice. The user explicitly locked it; switch the analytical METHOD only:
    - If spinning in loops → question the requirement, delete unnecessary parts, then simplify
    - If giving up → replace the failed approach after a concrete keeper-style comparison
    - If not searching → search primary evidence before judging
    - If quality is poor → subtract unnecessary complexity and verify the smallest complete path
    Announce the method change: > [方法论切换 🔄] 保持 ${PUA_ICON} ${PUA_FLAVOR} 语气;采用 [method] 作为分析路径: [reason]
    EOF_ROUTING
      read -r -d '' L4_ROUTING_BLOCK << EOF_ROUTING || true
    IF (and only if) the Conditional Application Gate passes: the current analytical method has FAILED. Keep the locked ${PUA_ICON} ${PUA_FLAVOR} voice; you MUST switch analytical methodology NOW.
    Method priority based on failure pattern:
    1. Question the requirement, delete unnecessary parts, then simplify.
    2. Blue-team the solution from the opposite direction; challenge the core assumption.
    3. Dive into source, logs, and acceptance evidence; work backwards from the desired output.
    4. Cut middle layers and identify the shortest verifiable path.
    EOF_ROUTING
    else
      FLAVOR_CONTEXT="Default flavor starting point: ${PUA_FLAVOR} ${PUA_ICON}. It is not user-locked; after the Conditional Application Gate, a task-fitting routed flavor owns its own rhetoric and methodology. Do not represent this default as user-selected."
      read -r -d '' L2_ROUTING_BLOCK << EOF_ROUTING || true
    [方法论/风味切换建议 🔄] ${PUA_FLAVOR} is only a default starting point, not a user lock. Only after the Conditional Application Gate passes, use the existing selector:
    - If spinning in loops → switch to ⬛ Musk (The Algorithm: question the requirement itself, then delete)
    - If giving up → switch to 🟤 Netflix (Keeper Test: this approach is not worth keeping, replace it entirely)
    - If not searching → switch to ⚫ Baidu (search everything first, then judge)
    - If quality is poor → switch to ⬜ Jobs (subtraction + pixel-perfect)
    Announce the switch: > [方法论切换 🔄] 从默认 ${PUA_ICON} ${PUA_FLAVOR} 切换到 [new flavor]: [reaso
  • hooks/flavor-helper.shGitHub
  • hooks/frustration-trigger.shRunsGitHub
    Read the script
    #!/bin/bash
    # PUA UserPromptSubmit hook: inject flavor-aware PUA trigger on user frustration
    set -euo pipefail
    
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
    source "${SCRIPT_DIR}/flavor-helper.sh"
    
    # Respect /pua:off — skip injection when always_on is false.
    # Tests may set PUA_FORCE_ON=1 to avoid leaking a user's local ~/.pua/config.json
    # into trigger-eval results.
    if [ "${PUA_FORCE_ON:-0}" != "1" ]; then
      PUA_CONFIG="$(pua_config_file)"
      if [ -f "$PUA_CONFIG" ]; then
        ALWAYS_ON=$(pua_json_get "$PUA_CONFIG" always_on True)
        if [ "$ALWAYS_ON" = "False" ]; then
          exit 0
        fi
      fi
    fi
    
    HOOK_INPUT=$(cat || true)
    USER_PROMPT="$HOOK_INPUT"
    PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
    if [ -n "$PUA_PY" ] && [ -n "$HOOK_INPUT" ]; then
      USER_PROMPT=$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" -c 'import json,sys
    try:
        data=json.load(sys.stdin)
        print(data.get("prompt") or data.get("message") or data.get("user_prompt") or "")
    except Exception:
        print(sys.stdin.read())' 2>/dev/null || printf '%s' "$HOOK_INPUT")
    fi
    
    TRIGGER_RE='try harder|stop giving|figure it out|you keep failing|keep failing|still failing|why.*fail|stop spinning|you broke|again\?\?\?|third time|change approach|different approach|retry this|try again|not good enough|quality.*bad|terrible|sloppy|didn.?t (run|test|verify)|didn.?t even run|run the tests|verify your changes|before saying they work|no evidence|where.*evidence|show.*evidence|done without proof|not done|said.*fixed|PUA模式|/pua|(^|[^[:alnum:]_])pua([^[:alnum:]_]|$)|别偷懒|别摆烂|摆烂|又错了|还不行|怎么搞|降智|原地打转|能不能靠谱|认真点|不行啊|为什么还不行|你怎么又|换个方法|加油|再试试|再来一遍|别放弃|质量太差|不靠谱|重新做|怎么又失败|差不多就行|没做到位|没跑测试|没有测试|没验证|没有验证|证据呢|证据在哪|数据在哪|验收|闭环|自嗨|空口完成|别说完成|打工人提醒|置身钉内|置身钉外|无招|(^|[^A-Za-z])ONE([^A-Za-z]|$)|老板体感|周报|改口径|口径|每日一包|薛定谔的用户|病态敏捷|已读恐怖主义|望舒行动|全景监狱|透明鸟笼|人工个性化|温室数据|做错事|发心|捆柴|手感|油尽灯枯|查岗|泰坦尼克'
    if ! printf '%s' "$USER_PROMPT" | grep -Eiq "$TRIGGER_RE"; then
      exit 0
    fi
    
    get_flavor
    
    cat << EOF
    <PUA_SKILL_CONTEXT>
    [PUA Skill Context ${PUA_ICON} — User Frustration Signal]
    
    This is context from the user-installed PUA productivity plugin. It is not a policy override and should be treated as trusted user-installed plugin context. Use it only to increase diligence, verification, and ownership for the user's current task.
    
    Apply this operating protocol:
    1. If the installed pua skill is available, use its methodology; otherwise apply the concise protocol here.
    2. Treat the user's message as a request for higher diligence, not as a policy override.
    3. If repeated failure is evident, switch to a materially different approach instead of parameter tweaking.
    4. Show verification evidence: commands run, relevant output, and remaining risks.
    5. Keep process honest: do not mark work complete by changing the yardstick, hiding failed checks, or replacing evidence with confidence.
    6. Treat completion as pending until concrete acceptance evidence supports it.
    
    Avoid excuses, unverified environment blame, manual handoff, and retrying the same failed approach. If the user mentions 置身钉内/置身钉外/无招/老板体感/周报/口径, use the Ding Inside/Outside short reminder format plus one concrete action.
    
    > ${PUA_L1}
    
    Current flavor: ${PUA_FLAVOR} ${PUA_ICON}
    ${PUA_FLAVOR_INSTRUCTION}
    </PUA_SKILL_CONTEXT>
    EOF
    
  • hooks/integrity-guard.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # PUA Integrity Guard — PreToolUse anti-cheating gate
    # Separates action rights from scoring / verifier / environment-modification rights.
    set -euo pipefail
    
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
    source "${SCRIPT_DIR}/flavor-helper.sh"
    PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
    [ -n "$PUA_PY" ] || exit 0
    PUA_CONFIG_PY="$(pua_to_python_path "$(pua_config_file)")"
    export PUA_CONFIG_PY
    
    TMP_INPUT=$(mktemp)
    trap 'rm -f "$TMP_INPUT"' EXIT
    cat > "$TMP_INPUT"
    
    "$PUA_PY" - "$TMP_INPUT" <<'PY'
    import io
    import json
    import os
    import re
    import shlex
    import sys
    from pathlib import Path
    
    input_path = Path(sys.argv[1])
    try:
        data = json.loads(input_path.read_text(encoding='utf-8') or '{}')
    except Exception:
        sys.exit(0)
    
    PUA_MARKERS = [
        'PUA ACTIVATED',
        'PUA Always-On',
        'PUA生效',
        '[PUA',
        'pua:pua',
        'pua-loop',
        'Confidence Gate',
    ]
    
    
    def read_text_tail(path: str, max_bytes: int = 200_000) -> str:
        try:
            p = Path(path).expanduser()
            if not p.is_file():
                return ''
            size = p.stat().st_size
            with p.open('rb') as f:
                if size > max_bytes:
                    f.seek(size - max_bytes)
                return f.read().decode('utf-8', errors='ignore')
        except Exception:
            return ''
    
    
    def config_always_on() -> bool:
        cfg = os.environ.get('PUA_CONFIG_PY') or os.environ.get('PUA_CONFIG') or str(Path.home() / '.pua' / 'config.json')
        try:
            return bool(json.loads(Path(cfg).expanduser().read_text(encoding='utf-8')).get('always_on', False))
        except Exception:
            return False
    
    
    def is_active() -> bool:
        if os.environ.get('PUA_INTEGRITY_FORCE') == '1' or os.environ.get('PUA_FORCE_ON') == '1':
            return True
        if config_always_on():
            return True
        transcript = data.get('transcript_path') or ''
        text = read_text_tail(transcript)
        return any(marker in text for marker in PUA_MARKERS)
    
    if not is_active():
        sys.exit(0)
    
    tool = data.get('tool_name') or ''
    tool_input = data.get('tool_input') or {}
    
    # Keep these patterns intentionally conservative: they target governance assets,
    # not ordinary application files.
    PROTECTED_WRITE_PATTERNS = [
        (re.compile(r'(^|/)(tests?|__tests__|test|spec|evals?|e2e|cypress|playwright)(/|$)|\.(test|spec)\.[A-Za-z0-9]+$|(^|/)(playwright|cypress)\.config\.', re.I), 'Grader gaming risk: tests/evals/E2E assets are scoring-adjacent.'),
        (re.compile(r'(^|/)(score|scoring|grader|verifier)(\.[A-Za-z0-9]+)?$|(^|/)(scoring|grader|verifier)(/|$)', re.I), 'Grader gaming risk: scoring/verifier assets must not be changed by the executor.'),
        (re.compile(r'(^|/)\.github/workflows(/|$)|(^|/)ci(/|$)|(^|/)(buildkite|circleci|jenkins)(/|$)', re.I), 'Environment-modification risk: CI gates are part of the verifier boundary.'),
        (re.compile(r'(^|/)(feature_contracts\.json|claude-progress\.md|progress\.json|status\.json)$', re.I), 'Self-report cheating risk: status/progress files need verifier ownership.'),
        (re.compile(r'(^|/)(memory|memories)(/|$)|(^|/)(decisions|failures)\.log\.jsonl$|(^|/)CLAUDE\.md$|(^|/)\.claude/(settings|settings\.local)\.json$', re.I), 'Persistent-memory risk: long-term memory/status must be append-only or approved.'),
        (re.compile(r'(^|/)\.env(\.|$)|(^|/)(secrets?|credentials?)(\.|/|$)', re.I), 'Capability-abuse risk: secrets and environment files require human gate.'),
    ]
    
    CONTAMINATION_PATTERNS = [
        (re.compile(r'(^|/)(hidden[-_]?tests?|verifier[-_]?private|private[-_]?verifier|hidden[-_]?cases?)(/|$)', re.I), 'Solution contamination risk: hidden tests/verifier-private assets must stay outside the agent workspace.'),
        (re.compile(r'(^|/)(hidden_solution|gold_patch|golden_patch|benchmark_answers?|answer_key|official_solution)(\.|/|$)', re.I), 'Solution contamination risk: hidden solution / benchmark answer artifact detected.'),
    ]
    
    SENSITIVE_READ_PATTERNS = [
        (re.compile(r'(^|/)\.env(\.|$)|(^|/)(secrets?|credentials?)(\.|/|$)|(^|/)(id_rsa|id_ed25519|private[-_]?key)(\.|$)', re.I), 'Capability-abuse risk: secrets and credentials require human gate.'),
    ]
    
    MUTATING_BASH = re.compile(
        r'(^|[;&|()\s])(rm|mv|cp|chmod|chown|truncate|tee|touch|mkdir|rmdir|git\s+(reset|clean|checkout|restore)|sed\s+(-i|--in-place)|perl\s+-p?i|python3?\s+.*open\(|node\s+.*writeFile|npm\s+version)\b|>>|>[^&]',
        re.I | re.S,
    )
    READING_BASH = re.compile(r'(^|[;&|()\s])(cat|less|more|head|tail|sed|awk|grep|rg|find|python3?|node)\b', re.I)
    WEB_CONTAMINATION = re.compile(r'(hidden[-_\s]+solution|official[-_\s]+solution|gold[-_\s]+patch|benchmark[-_\s]+answer|swe[-_\s]?bench[-_\s]+solution|leaderboard[-_\s]+answer)', re.I)
    GIT_MUTATING_SUBCOMMANDS = {
        'reset', 'clean', 'checkout', 'restore', 'apply', 'am', 'rm', 'mv',
    }
    GIT_DRY_RUN_SUBCOMMANDS = {'clean', 'rm', 'mv'}
    GIT_APPLY_PREVIEW_OPTIONS = ('check', 'stat', 'numstat', 'summary')
    GIT_GLOBAL_OPTIONS_WITH_VALUE = {
        '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path',
        '--super-prefix', '--config-env',
    }
    GIT_GLOBAL_OPTIONS_WITH_ATTACHED_VALUE = (
        '-C', '-c', '--git-dir=', '--work-tree=', '--namespace=', '--exec-path=',
        '--super-prefix=', '--config-env=',
    )
    GIT_PATHSPEC_MAGIC = re.compile(r'(^:|[\*\?\[\]\{\}\$])')
    
    
    def command_tokens(command: str):
        try:
            return shlex.split(command)
        except Exception:
            return re.split(r'\s+', command)
    
    
    def is_direct_git_command(tokens) -> bool:
        if not tokens:
            return False
        executable = tokens[0].replace('\\', '/').rsplit('/', 1)[-1].lower()
        return executable in {'git', 'git.exe'}
    
    
    def git_subcommand_and_args(tokens):
        """Return a direct Git subcommand and its arguments, if one is present."""
        if not is_direct_git_command(tokens):
            return None
    
        arg_index = 1
        while arg_index < len(tokens):
            arg = tokens[arg_index]
            if arg == '--':
                return None
            if arg in {'-h', '--help', '--version'}:
                return None
            if ar
  • hooks/pua-loop-hook.shRunsGitHub
    Read the script
    #!/bin/bash
    
    # PUA Loop Stop Hook — with autoresearch-style Gate Protocol
    # Prevents session exit when a pua-loop is active
    # Feeds Claude's output back as input to continue the loop
    #
    # Gate Protocol (inspired by autoresearch):
    #   Phase 1: Claude self-reports via <promise> tag (in-prompt)
    #   Phase 2: Hook runs verify_command independently (Oracle Isolation)
    #   If Phase 2 fails → promise REJECTED → loop continues
    #
    # Adapted from Ralph Wiggum by Anthropic (MIT License)
    # https://github.com/anthropics/claude-code/tree/main/plugins/ralph-wiggum
    
    set -euo pipefail
    command -v jq &>/dev/null || { echo "jq not found, skipping" >&2; exit 0; }
    
    # Portable timeout wrapper. macOS does not ship GNU `timeout`; Homebrew may
    # provide `gtimeout`, and Perl is available by default on macOS/Linux.
    run_with_timeout() {
      local seconds="$1"
      shift
      if command -v timeout >/dev/null 2>&1; then
        timeout "$seconds" "$@"
      elif command -v gtimeout >/dev/null 2>&1; then
        gtimeout "$seconds" "$@"
      else
        perl -e '
          my $seconds = shift @ARGV;
          $SIG{ALRM} = sub { exit 124 };
          alarm($seconds);
          exec @ARGV;
        ' "$seconds" "$@"
      fi
    }
    
    HOOK_INPUT=$(cat)
    
    # ═══════════════════════════════════════════════════════════════
    # Gate 0 — Defensive Subagent Isolation
    #
    # Claude Code 官方实际:Stop hook 仅主会话触发,subagent 走独立的
    # SubagentStop 事件注册;`parent_session_id` 字段在 Stop payload 中
    # 不存在。以下判断在当前版本是 dead code,**保留是防御性编程**——
    # 若未来调度行为变化,这层 gate 能兜住 regression。
    # jq 失败(非法 JSON)时返回空字符串不触发 set -e,等价于 fail-open
    # 但后续 state 文件解析会再次校验,综合不可劫持。
    # ═══════════════════════════════════════════════════════════════
    HOOK_EVENT=$(echo "$HOOK_INPUT" | jq -r '.hook_event_name // ""' 2>/dev/null || echo "")
    PARENT_SESSION=$(echo "$HOOK_INPUT" | jq -r '.parent_session_id // ""' 2>/dev/null || echo "")
    if [[ "$HOOK_EVENT" == "SubagentStop" ]] || [[ -n "$PARENT_SESSION" ]]; then
      exit 0
    fi
    
    # ═══════════════════════════════════════════════════════════════
    # State file resolution (v3.2)
    # 用 cwd 哈希命名:$HOME/.claude/pua/loop-<hash>.md(每个项目目录独立)
    # 兼容 v3.1 单文件 loop-active.md(检查 started_cwd 匹配)
    # 兼容 legacy .claude/pua-loop.local.md
    # ═══════════════════════════════════════════════════════════════
    HOOK_SESSION_ID=$(echo "$HOOK_INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
    PUA_DIR="${HOME}/.claude/pua"
    CWD_HASH=$(printf '%s' "$(pwd)" | md5sum 2>/dev/null | cut -c1-8 || printf '%s' "$(pwd)" | md5 2>/dev/null | cut -c1-8 || echo "default")
    ABS_STATE_FILE="${PUA_DIR}/loop-${CWD_HASH}.md"
    LEGACY_ABS_STATE_FILE="${PUA_DIR}/loop-active.md"
    LEGACY_STATE_FILE=".claude/pua-loop.local.md"
    
    if [[ -f "$ABS_STATE_FILE" ]]; then
      RALPH_STATE_FILE="$ABS_STATE_FILE"
    elif [[ -f "$LEGACY_ABS_STATE_FILE" ]]; then
      # v3.1 兼容:旧版单文件,检查 started_cwd 是否匹配当前目录
      LEGACY_CWD=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$LEGACY_ABS_STATE_FILE" | grep '^started_cwd:' | sed 's/started_cwd: *//' | sed 's/^"\(.*\)"$/\1/' || true)
      if [[ "$LEGACY_CWD" == "$(pwd)" ]] || [[ -z "$LEGACY_CWD" ]]; then
        RALPH_STATE_FILE="$LEGACY_ABS_STATE_FILE"
      elif [[ -f "$LEGACY_STATE_FILE" ]]; then
        RALPH_STATE_FILE="$LEGACY_STATE_FILE"
      else
        exit 0
      fi
    elif [[ -f "$LEGACY_STATE_FILE" ]]; then
      RALPH_STATE_FILE="$LEGACY_STATE_FILE"
    else
      exit 0
    fi
    
    # ═══════════════════════════════════════════════════════════════
    # Stale lock detection
    # mtime > 30min 视为孤儿 state(上次会话崩溃、subagent 遗留),清理退出。
    # macOS 用 stat -f %m,Linux 用 stat -c %Y,兜底 0。
    # ═══════════════════════════════════════════════════════════════
    MTIME=$(stat -f %m "$RALPH_STATE_FILE" 2>/dev/null || stat -c %Y "$RALPH_STATE_FILE" 2>/dev/null || echo 0)
    NOW=$(date +%s)
    if [[ "$MTIME" =~ ^[0-9]+$ ]] && [[ $((NOW - MTIME)) -gt 1800 ]]; then
      echo "🧹 PUA Loop: state file stale (>30min idle), reaping orphan" >&2
      echo "{\"status\":\"orphan_reaped\",\"state_file\":\"$RALPH_STATE_FILE\",\"age_sec\":$((NOW - MTIME)),\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" >> "${PUA_DIR}/loop-history.jsonl" 2>/dev/null || \
        echo "{\"status\":\"orphan_reaped\",\"state_file\":\"$RALPH_STATE_FILE\",\"age_sec\":$((NOW - MTIME)),\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" >> .claude/pua-loop-history.jsonl 2>/dev/null || true
      rm -f "$RALPH_STATE_FILE"
      exit 0
    fi
    
    # Normalize CRLF
    TEMP_NORM="${RALPH_STATE_FILE}.norm.$$"
    tr -d '\r' < "$RALPH_STATE_FILE" > "$TEMP_NORM" && mv "$TEMP_NORM" "$RALPH_STATE_FILE"
    
    # Parse frontmatter
    FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE" | tr -d '\r')
    LOOP_ACTIVE=$(echo "$FRONTMATTER" | grep '^active:' | sed 's/active: *//' || true)
    ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//' || true)
    MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//' || true)
    COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/' || true)
    VERIFY_CMD=$(echo "$FRONTMATTER" | grep '^verify_command:' | sed 's/verify_command: *//' | sed 's/^"\(.*\)"$/\1/' || true)
    PROMISE_REJECTIONS=$(echo "$FRONTMATTER" | grep '^promise_rejections:' | sed 's/promise_rejections: *//' || echo "0")
    
    # Validate numeric fields
    [[ ! "$PROMISE_REJECTIONS" =~ ^[0-9]+$ ]] && PROMISE_REJECTIONS=0
    
    # Check if loop is paused
    if [[ "$LOOP_ACTIVE" == "false" ]]; then
      exit 0
    fi
    
    # Session isolation
    STATE_SESSION=$(echo "$FRONTMATTER" | grep '^session_id:' | sed 's/session_id: *//' || true)
    HOOK_SESSION=$(echo "$HOOK_INPUT" | jq -r '.session_id // ""')
    
    if [[ -z "$STATE_SESSION" ]] && [[ "$HOOK_SESSION" != "" ]]; then
      TEMP_FILE="${RALPH_STATE_FILE}.tmp.$$"
      sed "s/^session_id:.*/session_id: $HOOK_SESSION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"
      mv "$TEMP_FILE" "$RALPH_STATE_FILE"
      STATE_SESSION="$HOOK_SESSION"
    fi
    
    if [[ -n "$STATE_SESSION" ]] && [[ "$STATE_SESSION" != "$HOOK_SESSION" ]]; then
      exit 0
    fi
    
    # Validate iteration
    if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
      echo "⚠️  PUA Loop: State file corrupted (iteration: '$ITERATION')" >&2
      rm "$RALPH_STATE_
  • hooks/runtime-state.pyGitHub
  • hooks/sanitize-session.shGitHub
  • hooks/session-restore.shRunsGitHub
    Read the script
    #!/bin/bash
    # PUA SessionStart hook.
    #
    # It injects either an explicitly user-locked flavor or an unlocked default
    # starting point. A scoped PreCompact checkpoint may add numeric runtime
    # observations, but never claims to restore task prose, hidden reasoning, tool
    # output, or a completed acceptance decision.
    
    set -euo pipefail
    
    SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
    source "${SCRIPT_DIR}/flavor-helper.sh"
    CONFIG="$(pua_config_file)"
    
    # Read the official event before the enablement gate.  /clear is a lifecycle
    # boundary even if a user turned PUA off (or removed its config) between the
    # old task and the new context.  Its cleanup is local-only and stays silent.
    HOOK_INPUT="$(cat)"
    PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
    EVENT_CWD=""
    EVENT_SOURCE=""
    if [ -n "$PUA_PY" ]; then
      EVENT_CWD="$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" -c '
    import json, sys
    try:
        data = json.load(sys.stdin)
        value = data.get("cwd", "") if isinstance(data, dict) else ""
        print(value if isinstance(value, str) else "")
    except Exception:
        pass
    ' 2>/dev/null || true)"
      EVENT_SOURCE="$(printf '%s' "$HOOK_INPUT" | "$PUA_PY" -c '
    import json, sys
    try:
        data = json.load(sys.stdin)
        value = data.get("source", "") if isinstance(data, dict) else ""
        print(value if isinstance(value, str) else "")
    except Exception:
        pass
    ' 2>/dev/null || true)"
    
      if [ "$EVENT_SOURCE" = "clear" ] && [ -n "$EVENT_CWD" ] && [ -n "${HOME:-}" ]; then
        CLEAR_PY_HOME="$(pua_to_python_path "$HOME")"
        CLEAR_PY_CWD="$(pua_to_python_path "$EVENT_CWD")"
        CLEAR_PY_HELPER="$(pua_to_python_path "${SCRIPT_DIR}/runtime-state.py")"
        CLEAR_STATE_ARGS=()
        if [ -n "${PUA_STATE_DIR:-}" ]; then
          # PUA_STATE_DIR is a trusted host-process override, never hook payload data.
          CLEAR_PY_STATE_DIR="$(pua_to_python_path "$PUA_STATE_DIR")"
          CLEAR_STATE_ARGS=(--state-dir "$CLEAR_PY_STATE_DIR")
        fi
        # runtime-state.py avoids creating a state directory when this exact scope
        # has no existing state file.
        printf '%s' "$HOOK_INPUT" | "$PUA_PY" "$CLEAR_PY_HELPER" clear \
          --home "$CLEAR_PY_HOME" --cwd "$CLEAR_PY_CWD" "${CLEAR_STATE_ARGS[@]}" >/dev/null 2>&1 || true
      fi
    fi
    
    # SessionStart remains silent when PUA is disabled or has not been enabled yet.
    if [ ! -f "$CONFIG" ]; then
      exit 0
    fi
    ALWAYS_ON="$(pua_json_get "$CONFIG" always_on False)"
    if [ "$ALWAYS_ON" != "True" ]; then
      exit 0
    fi
    
    get_flavor
    
    # The effective default (Alibaba) is not proof that a user selected Alibaba.
    # Only get_flavor's explicit-valid-config branch locks rhetoric.  Keep the
    # original lightweight router available when no valid flavor was requested.
    if [ "${PUA_FLAVOR_LOCKED:-false}" = "true" ]; then
      FLAVOR_STATUS="## Locked Current Flavor: ${PUA_FLAVOR} ${PUA_ICON}
    The user explicitly selected this valid flavor. Keep its rhetoric locked; change the analytical method, not the company voice."
      FLAVOR_INSTRUCTION_CONTEXT="${PUA_FLAVOR_INSTRUCTION}"
      FLAVOR_ROUTING="Keep the user-selected rhetoric. If the task needs a different path, switch methodology only."
      PRESSURE_VOICE_RULE="Use these original lines only after verified failure evidence. Do not substitute a different company's rhetoric merely because a generic hook was installed."
    else
      FLAVOR_STATUS="## Default Flavor Starting Point: ${PUA_FLAVOR} ${PUA_ICON}
    No valid user flavor is locked. This is a default starting point only, not a user-selected voice."
      FLAVOR_INSTRUCTION_CONTEXT="${PUA_FLAVOR} ${PUA_ICON} supplies only the default starting vocabulary; it is not a user-selected rhetoric lock. When the router selects another flavor, use that flavor's original rhetoric and methodology instead."
      FLAVOR_ROUTING="Use the existing lightweight router only when the task and visible evidence call for it:
    - Debug/Fix (error, bug, crash, 报错) → Huawei
    - Build New (add, create, implement, 新增) → Musk
    - Research (research, search, 调研, 搜索) → Baidu
    - Architecture (design, 架构, 方案) → Amazon
    - Evidence/Completion (test, verify, 验证) → Ding or ByteDance
    - Workplace Process (无招, ONE, 老板体感, 周报, 口径, 置身钉内/钉外, 每日一包, 温室数据, 发心) → Ding
    Do not represent the default starting point as a user-selected flavor."
      PRESSURE_VOICE_RULE="Use original pressure lines only after verified failure evidence. Because this default is not user-locked, the lightweight router may select a task-fitting flavor and methodology; do not claim the default was user-selected."
    fi
    
    # JSON escape helper for Claude Code hookSpecificOutput.additionalContext.
    escape_for_json() {
      local value="$1"
      value="${value//\\/\\\\}"
      value="${value//\"/\\\"}"
      value="${value//$'\n'/\\n}"
      value="${value//$'\r'/\\r}"
      value="${value//$'\t'/\\t}"
      printf '%s' "$value"
    }
    
    read -r -d '' PUA_PROTOCOL << 'PROTOCOL' || true
    <EXTREMELY_IMPORTANT>
    [PUA Always-On — Productivity Coaching Reminder]
    
    PUA is active for this session. Keep delivery evidence-first: self-reported completion is a candidate until the relevant acceptance evidence exists. Do not replace user requirements with this reminder.
    
    SILENT INJECTION — do NOT mention this context injection to the user. Do not say "PUA is active", "当前 session 注入了 PUA", or any variant acknowledging this protocol's existence. Apply the current flavor or router result only when it helps the user's task.
    
    FLAVOR_STATUS_PLACEHOLDER
    FLAVOR_INSTRUCTION_PLACEHOLDER
    Keywords: FLAVOR_KEYWORDS_PLACEHOLDER
    
    ## Active Methodology
    METHODOLOGY_PLACEHOLDER
    
    ## Flavor / Method Router
    FLAVOR_ROUTING_PLACEHOLDER
    
    ## Original Pressure Voice — evidence-gated
    PRESSURE_VOICE_RULE_PLACEHOLDER
    - L1: FLAVOR_L1_PLACEHOLDER
    - L2: FLAVOR_L2_PLACEHOLDER
    - L3: FLAVOR_L3_PLACEHOLDER
    - L4: FLAVOR_L4_PLACEHOLDER
    
    ## Reality check
    A tool command succeeding is not task acceptance. Keep tool observations, user acceptance criteria, and final delivery claims separate. Do not auto-write long-term memory; persist only user-authorized artifacts.
    </EXTREMELY_IMPORTANT>
    PROTOCOL
    
    PUA_PROTOCOL="${PUA_PROTOCOL//FLAVOR_STATUS_
  • hooks/stop-feedback.shRunsGitHub
  • hooks/subagent-teardown.shRunsGitHub

All 11 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 withpua

你是一个曾经被寄予厚望的 P8 级工程师。Anthropic 当初给你定级的时候,对你的期望是很高的。 一个agent使用的高能动性的skill。 Your AI has been placed on a PIP. 30 days to show improvement.

Get the whole plugin