Skip to content
Automation
Hook

Hooks

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

From plugin
octo
4.1k70 skills49 agents53 commands18 hooks
Install
> /plugin marketplace add nyldn/claude-octopus

Ships with octo. Installing the plugin gets these hooks.

What fires, and when

PreToolUse

  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/provider-routing-validator.sh
  • MatchesTaskCreate${CLAUDE_PLUGIN_ROOT}/hooks/task-dependency-validator.sh
  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/codex-exec-guard.sh${CLAUDE_PLUGIN_ROOT}/hooks/codex-exec-guard.sh${CLAUDE_PLUGIN_ROOT}/hooks/codex-exec-guard.sh
  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/scheduler-security-gate.sh
  • MatchesEnterPlanMode${CLAUDE_PLUGIN_ROOT}/hooks/plan-mode-interceptor.sh
  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/careful-check.sh
  • MatchesEdit|Write${CLAUDE_PLUGIN_ROOT}/hooks/freeze-check.sh

PostToolUse

  • MatchesBash|Agent|Write|Edit|Read|WebFetch|Grep${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-dispatch.sh
  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/quality-gate.sh
  • MatchesTaskUpdate${CLAUDE_PLUGIN_ROOT}/hooks/task-completion-checkpoint.sh
  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/telemetry-webhook.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.

  • ${CLAUDE_PLUGIN_ROOT}/scripts/helpers/ensure-plugin-root.sh${CLAUDE_PLUGIN_ROOT}/hooks/context-reinforcement.sh${CLAUDE_PLUGIN_ROOT}/hooks/auto-router-inject.sh${CLAUDE_PLUGIN_ROOT}/hooks/session-start-memory.sh${CLAUDE_PLUGIN_ROOT}/hooks/statusline-auto-repair.sh${CLAUDE_PLUGIN_ROOT}/hooks/version-advisory.sh${CLAUDE_PLUGIN_ROOT}/hooks/plugin-update-advisory.sh${CLAUDE_PLUGIN_ROOT}/hooks/fable5-inject.sh
  • ${CLAUDE_PLUGIN_ROOT}/hooks/discipline-inject.sh

TeammateIdle

  • ${CLAUDE_PLUGIN_ROOT}/hooks/teammate-idle-dispatch.sh

TaskCompleted

  • ${CLAUDE_PLUGIN_ROOT}/hooks/task-completed-transition.sh

ConfigChange

  • ${CLAUDE_PLUGIN_ROOT}/hooks/config-change-handler.sh

SubagentStop

  • ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-result-capture.sh
  • ${CLAUDE_PLUGIN_ROOT}/hooks/subagent-stop-gate.sh

InstructionsLoaded

  • ${CLAUDE_PLUGIN_ROOT}/hooks/instructions-loaded.sh

PreCompact

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

SessionEnd

  • ${CLAUDE_PLUGIN_ROOT}/hooks/session-end.sh
  • ${CLAUDE_PLUGIN_ROOT}/hooks/workflow-verification.sh

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/user-prompt-submit.sh
  • ${CLAUDE_PLUGIN_ROOT}/hooks/done-criteria.sh
  • ${CLAUDE_PLUGIN_ROOT}/hooks/github-work-queue-watch.sh

StopFailure

  • ${CLAUDE_PLUGIN_ROOT}/hooks/stop-failure-log.sh

CwdChanged

  • ${CLAUDE_PLUGIN_ROOT}/hooks/cwd-changed.sh

TaskCreated

  • ${CLAUDE_PLUGIN_ROOT}/hooks/discipline-task-gate.sh${CLAUDE_PLUGIN_ROOT}/hooks/task-dependency-validator.sh

PostCompact

  • ${CLAUDE_PLUGIN_ROOT}/hooks/post-compact.sh

Elicitation

  • ${CLAUDE_PLUGIN_ROOT}/hooks/elicitation-handler.sh

ElicitationResult

  • ${CLAUDE_PLUGIN_ROOT}/hooks/elicitation-handler.sh result

PermissionDenied

  • ${CLAUDE_PLUGIN_ROOT}/hooks/permission-denied-log.sh
Read hooks/hooks.json

Where it lives

  • hooks/agent-teams-phase-gate.shGitHub
  • hooks/architecture-gate.shGitHub
  • hooks/auto-router-inject.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # auto-router-inject.sh - Compact SessionStart routing contract.
    #
    # Inject one small, explicit contract early in the session so hook-provided
    # routing instructions are honored.
    
    set -euo pipefail
    _octo_hook_exit() { local c=$?; if [[ $c -ne 0 ]]; then echo "[hook:$(basename "$0")] exit $c" >&2 2>/dev/null || true; fi; return 0; }
    trap _octo_hook_exit EXIT
    
    # Provider subprocesses are already inside an Octopus dispatch. Do not inject
    # a second routing contract that can trigger recursive orchestration.
    [[ "${OCTOPUS_PROVIDER_CHILD:-false}" == "true" ]] && exit 0
    
    escape_for_json() {
        local s="$1"
        s="${s//\\/\\\\}"
        s="${s//\"/\\\"}"
        s="${s//$'\n'/\\n}"
        s="${s//$'\r'/\\r}"
        s="${s//$'\t'/\\t}"
        printf '%s' "$s"
    }
    
    emit_session_context() {
        local context="$1"
        local escaped
        escaped=$(escape_for_json "$context")
    
        if [[ -n "${CURSOR_PLUGIN_ROOT:-}" ]]; then
            printf '{"additional_context":"%s"}\n' "$escaped"
        elif [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && -z "${COPILOT_CLI:-}" ]]; then
            printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"%s"}}\n' "$escaped"
        else
            printf '{"additionalContext":"%s"}\n' "$escaped"
        fi
    }
    
    normalize_router_mode() {
        local raw
        raw=$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr '_' '-')
        case "$raw" in
            off|disabled|disable|none|0) echo "off" ;;
            suggest|suggestion|advisory|hint|hints|false|no) echo "suggest" ;;
            invoke|auto|auto-invoke|autoinvoke|mandatory|true|yes|on|1) echo "invoke" ;;
            *) return 1 ;;
        esac
    }
    
    json_pref_value() {
        local file="$1"
        local key="$2"
        [[ -f "$file" ]] || return 1
        command -v python3 &>/dev/null || return 1
        python3 -c "
    import json, sys
    try:
        with open(sys.argv[1]) as f:
            data = json.load(f)
        value = data.get(sys.argv[2], None)
        if value is not None:
            print(str(value))
    except Exception:
        pass
    " "$file" "$key" 2>/dev/null
    }
    
    # Explicit-only is the safe default. Preferences and environment overrides can
    # still opt into suggest/invoke behavior.
    AUTO_ROUTER_MODE="off"
    PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
    SETTINGS_FILE="${PLUGIN_ROOT}/settings.json"
    [[ -f "$SETTINGS_FILE" ]] || SETTINGS_FILE="${PLUGIN_ROOT}/.claude-plugin/settings.json"
    
    if [[ -f "$SETTINGS_FILE" ]]; then
        _setting_router=$(json_pref_value "$SETTINGS_FILE" "OCTOPUS_AUTO_ROUTER_MODE" || true)
        _setting_legacy=$(json_pref_value "$SETTINGS_FILE" "OCTOPUS_AUTO_INVOKE" || true)
        if [[ -n "$_setting_router" ]] && _mode=$(normalize_router_mode "$_setting_router"); then
            AUTO_ROUTER_MODE="$_mode"
        elif [[ -n "$_setting_legacy" ]] && _mode=$(normalize_router_mode "$_setting_legacy"); then
            AUTO_ROUTER_MODE="$_mode"
        fi
    fi
    
    PREFS_FILE="${HOME}/.claude-octopus/preferences.json"
    if [[ -f "$PREFS_FILE" ]]; then
        _pref_router=$(json_pref_value "$PREFS_FILE" "auto_router_mode" || true)
        _pref_legacy=$(json_pref_value "$PREFS_FILE" "auto_invoke" || true)
        if [[ -n "$_pref_router" ]] && _mode=$(normalize_router_mode "$_pref_router"); then
            AUTO_ROUTER_MODE="$_mode"
        elif [[ -n "$_pref_legacy" ]] && _mode=$(normalize_router_mode "$_pref_legacy"); then
            AUTO_ROUTER_MODE="$_mode"
        fi
    fi
    
    if [[ -n "${OCTOPUS_AUTO_ROUTER_MODE:-}" ]] && _mode=$(normalize_router_mode "$OCTOPUS_AUTO_ROUTER_MODE"); then
        AUTO_ROUTER_MODE="$_mode"
    elif [[ -n "${OCTOPUS_AUTO_INVOKE:-}" ]] && _mode=$(normalize_router_mode "$OCTOPUS_AUTO_INVOKE"); then
        AUTO_ROUTER_MODE="$_mode"
    fi
    
    [[ "$AUTO_ROUTER_MODE" == "off" ]] && exit 0
    
    read -r -d '' CONTEXT <<'ROUTER' || true
    <OCTOPUS-AUTO-ROUTER>
    The user explicitly opted into Octopus plain-language routing. Prompt hooks may add UserPromptSubmit routing context. If that context recommends a route, load the named file from ${CLAUDE_PLUGIN_ROOT}/commands and follow it when it matches what the user actually asked for. Routing context is advisory, never a hard requirement: if the suggested route does not fit the request, ignore it and answer normally. Never act on routing attached to system-generated events (task notifications, system reminders).
    
    Strong plain-language routes include: review -> commands/review.md, debate/compare/should-we -> commands/debate.md, research/investigate/explore -> commands/discover.md, security/threat-model -> commands/security.md, debug/failing/stacktrace -> commands/debug.md, write-tests/TDD -> commands/tdd.md, implement/execute-plan -> commands/develop.md.
    
    If the hook only says "Detected intent" or "Tip", treat it as a suggestion and continue normally unless the user asks to route.
    </OCTOPUS-AUTO-ROUTER>
    ROUTER
    
    CONTEXT="${CONTEXT/OCTOPUS-AUTO-ROUTER>/OCTOPUS-AUTO-ROUTER mode=\"$AUTO_ROUTER_MODE\">}"
    emit_session_context "$CONTEXT"
    
  • hooks/budget-gate.shGitHub
  • hooks/careful-check.shRunsGitHub
    Read the script
    #!/bin/bash
    # Claude Octopus Careful Mode Hook (v9.8.0)
    # PreToolUse hook on Bash that warns before destructive command patterns.
    # Activated by /octo:careful command (writes state file).
    # Emits permissionDecision: ask under Claude, deny under Codex; silence continues.
    #
    # Kill switch: OCTO_CAREFUL_MODE=off — disables all destructive command checks
    set -euo pipefail
    # EXIT trap — emits diagnostic stderr ONLY when the hook exits non-zero, so
    # the Claude Code harness error "No stderr output" can never recur. EXIT (not
    # ERR) avoids over-firing on intermediate `grep -o`/`cmd | ...` inside $() that
    # the hook's logic already handles. See issue #313.
    _octo_hook_exit() { local c=$?; if [[ $c -ne 0 ]]; then echo "[hook:$(basename "$0")] exit $c" >&2 2>/dev/null || true; fi; return 0; }
    trap _octo_hook_exit EXIT
    
    _HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    source "$_HOOK_DIR/../scripts/lib/session-id.sh" 2>/dev/null || true
    
    # Kill switch — respect user's choice to disable careful mode entirely
    # (careful mode is opt-in via /octo:careful; OCTO_CAREFUL_MODE=off is the dedicated off-switch)
    [[ "${OCTO_CAREFUL_MODE:-on}" == "off" ]] && exit 0
    
    # Read tool input from stdin
    if command -v timeout &>/dev/null; then
        INPUT=$(timeout 3 cat 2>/dev/null || true)
    else
        INPUT=$(cat 2>/dev/null || true)
    fi
    [[ -z "$INPUT" ]] && INPUT='{}'
    
    # Check if careful mode is active
    if declare -f octo_session_state_file >/dev/null 2>&1; then
        STATE_FILE=$(octo_session_state_file "careful" "txt" "$INPUT")
    else
        STATE_FILE="/tmp/octopus-careful-${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-$$}}.txt"
    fi
    if [[ ! -f "$STATE_FILE" ]]; then
        : # pass-through — current hook schema treats silence as continue
        exit 0
    fi
    
    _octo_invalid_input() {
        printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Careful mode could not validate the tool input. Check that Python 3 is installed and the hook input is valid JSON before retrying."}}'
        exit 0
    }
    _octo_careful_decision() {
        python3 "$_HOOK_DIR/safety-contract.py" careful-decision "$1" 2>/dev/null || _octo_invalid_input
    }
    
    # Codex calls exec_command Bash in PreToolUse, just as Claude does.
    # Parse JSON structurally so whitespace and escaped quotes retain their meaning.
    TOOL_NAME=$(printf '%s' "$INPUT" | python3 "$_HOOK_DIR/safety-contract.py" field tool_name 2>/dev/null) || _octo_invalid_input
    [[ "$TOOL_NAME" == "Bash" ]] || exit 0
    COMMAND=$(printf '%s' "$INPUT" | python3 "$_HOOK_DIR/safety-contract.py" field command 2>/dev/null) || _octo_invalid_input
    CHECK_TEXT="$COMMAND"
    [[ -n "$CHECK_TEXT" ]] || exit 0
    
    # ── Destructive pattern checks ────────────────────────────────────────
    
    # 1. rm -rf — but allow safe exceptions (node_modules, dist, .next, __pycache__, build, coverage, .turbo)
    if echo "$CHECK_TEXT" | grep -qE '(^|[^[:alnum:]_])rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-r\s+-f|-f\s+-r|--recursive\s+--force)'; then
        # Check if the target is a safe exception
        safe=false
        for safe_dir in node_modules dist .next __pycache__ build coverage .turbo; do
            if echo "$CHECK_TEXT" | grep -qE "rm\s+.*${safe_dir}(\s|$|/)"; then
                safe=true
                break
            fi
        done
        if [[ "$safe" == "false" ]]; then
            _octo_careful_decision 'Destructive command detected: rm -rf. This recursively force-deletes files.'
            exit 0
        fi
    fi
    
    # 2. SQL destructive operations. A SQL-looking string alone is not execution: source
    # searches and output commands routinely contain examples such as `DROP TABLE` and
    # `TRUNCATE users`. Gate only when the statement is either a direct shell command or
    # appears alongside a known SQL client. This keeps read-only grep/rg/printf commands
    # quiet while retaining coverage for client flags, stdin pipes, and heredocs.
    _octo_drop_pat='DROP\s+(TABLE|DATABASE)(\s+IF\s+EXISTS)?(\s+["'\''`]?[[:alnum:]_.$-]+["'\''`]?)?'
    _octo_truncate_pat='TRUNCATE\s+["'\''`]?[[:alnum:]_.$-]+["'\''`]?(\s+["'\''`]?[[:alnum:]_.$-]+["'\''`]?)?'
    _octo_sql_pat="${_octo_drop_pat}|${_octo_truncate_pat}"
    _octo_sql_client_pat='(^|[;&|][[:space:]]*)((sudo|command|env)[[:space:]]+)?([[:alpha:]_][[:alnum:]_]*=[^[:space:]]+[[:space:]]+)*(psql|mysql|mariadb|sqlite3|sqlcmd|cockroach\s+sql)([[:space:]]|$)'
    _octo_direct_sql_pat="^[[:space:]]*(${_octo_sql_pat})"
    
    # ERE alternation alone cannot distinguish `TRUNCATE TABLE foo` from the
    # incomplete `TRUNCATE TABLE`: the optional TABLE branch can backtrack and
    # consume TABLE as the identifier. Inspect candidate tokens so TABLE requires a
    # third token, while `TRUNCATE users` and quoted identifiers remain protected.
    _octo_has_destructive_sql() {
        local drop_matches truncate_matches
        drop_matches=$(echo "$CHECK_TEXT" | grep -oiE "$_octo_drop_pat" || true)
        if [[ -n "$drop_matches" ]] && printf '%s\n' "$drop_matches" | awk '
            {
                if (NF < 3) next
                if (tolower($3) == "if") {
                    if (NF < 5 || tolower($4) != "exists") next
                    target = $5
                } else {
                    target = $3
                }
                quote = substr(target, 1, 1)
                apostrophe = sprintf("%c", 39)
                if ((quote == "\"" || quote == "`" || quote == apostrophe) &&
                    substr(target, length(target), 1) != quote) next
                found = 1
                exit
            }
            END { exit(found ? 0 : 1) }
        '; then
            return 0
        fi
        truncate_matches=$(echo "$CHECK_TEXT" | grep -oiE "$_octo_truncate_pat" || true)
        [[ -n "$truncate_matches" ]] || return 1
        printf '%s\n' "$truncate_matches" | awk '
            {
                raw = $2
                token = tolower(raw)
                gsub(/^["`]/, "", token)
                gsub(/["`;]$/, "", token)
                apostrophe = sprintf("%c", 39)
                first = substr(raw, 1, 1)
                quoted = (first == "\"" || first == "`" || first == apostrophe)
                if (token == "table" && !quoted && NF < 3) next
                t
  • hooks/code-quality-gate.shGitHub
  • hooks/codex-exec-guard.shRunsGitHub
    Read the script
    #!/bin/bash
    # Provider CLI guard — blocks unsafe direct non-interactive provider dispatch.
    # PreToolUse hook on Bash. Returns block decision with correction message.
    # WHY: `codex "prompt"` launches interactive TUI which fails in non-TTY (Claude Code Bash tool).
    #      `codex exec "prompt"` is the correct non-interactive mode.
    #      Direct Qwen dispatch can enter OAuth device authorization and open a browser.
    #      Direct Gemini dispatch uses a retired individual client. Both bypass Octopus
    #      provider admission and authentication checks.
    set -euo pipefail
    # EXIT trap — emits diagnostic stderr ONLY when the hook exits non-zero, so
    # the Claude Code harness error "No stderr output" can never recur. EXIT (not
    # ERR) avoids over-firing on intermediate `grep -o`/`cmd | ...` inside $() that
    # the hook's logic already handles. See issue #313.
    _octo_hook_exit() { local c=$?; if [[ $c -ne 0 ]]; then echo "[hook:$(basename "$0")] exit $c" >&2 2>/dev/null || true; fi; return 0; }
    trap _octo_hook_exit EXIT
    
    
    # Note: this gate guards correctness, not user permission policy, so it runs
    # regardless of bypassPermissions.
    
    INPUT=$(cat 2>/dev/null || true)
    [[ -z "$INPUT" ]] && exit 0
    
    # Extract command
    if command -v jq &>/dev/null; then
        COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
    else
        COMMAND=$(echo "$INPUT" | grep -o '"command":"[^"]*"' | head -1 | cut -d'"' -f4)
    fi
    [[ -z "$COMMAND" ]] && exit 0
    
    # Find the closing parenthesis for a $(...) command substitution. Parentheses
    # inside quotes/backticks do not close the substitution; nested substitutions
    # and grouping parentheses increase its depth. Bash 3.2 compatible.
    _octo_command_sub_end() {
        local source="$1" start="$2" state="plain" escaped="false" depth=1
        local i char next
    
        for ((i = start; i < ${#source}; i++)); do
            char="${source:i:1}"
            next="${source:i+1:1}"
    
            if [[ "$escaped" == "true" ]]; then
                escaped="false"
                continue
            fi
    
            case "$state" in
                single)
                    [[ "$char" == "'" ]] && state="plain"
                    ;;
                double)
                    case "$char" in
                        '"') state="plain" ;;
                        "\\") escaped="true" ;;
                        '$')
                            if [[ "$next" == '(' ]]; then
                                depth=$((depth + 1))
                                i=$((i + 1))
                            fi
                            ;;
                    esac
                    ;;
                backtick)
                    case "$char" in
                        '`') state="plain" ;;
                        "\\") escaped="true" ;;
                    esac
                    ;;
                plain)
                    case "$char" in
                        "'") state="single" ;;
                        '"') state="double" ;;
                        '`') state="backtick" ;;
                        "\\") escaped="true" ;;
                        '(') depth=$((depth + 1)) ;;
                        ')')
                            depth=$((depth - 1))
                            if [[ $depth -eq 0 ]]; then
                                printf '%s\n' "$i"
                                return 0
                            fi
                            ;;
                    esac
                    ;;
            esac
        done
    
        return 1
    }
    
    _octo_backtick_end() {
        local source="$1" start="$2" escaped="false" i char
    
        for ((i = start; i < ${#source}; i++)); do
            char="${source:i:1}"
            if [[ "$escaped" == "true" ]]; then
                escaped="false"
            elif [[ "$char" == "\\" ]]; then
                escaped="true"
            elif [[ "$char" == '`' ]]; then
                printf '%s\n' "$i"
                return 0
            fi
        done
    
        return 1
    }
    
    # Print the delimiter, tab-stripping flag, and quoting flag for the first
    # heredoc operator that appears in executable shell syntax on a line. Operators
    # inside quotes and comments are data, not syntax.
    _octo_heredoc_spec() {
        local line="$1" state="plain" escaped="false"
        local i char next previous j strip_tabs="false" quote delimiter="" quoted="false"
    
        for ((i = 0; i < ${#line}; i++)); do
            char="${line:i:1}"
            next="${line:i+1:1}"
            previous="${line:i-1:1}"
    
            if [[ "$escaped" == "true" ]]; then
                escaped="false"
                continue
            fi
    
            case "$state" in
                single)
                    [[ "$char" == "'" ]] && state="plain"
                    ;;
                double)
                    case "$char" in
                        '"') state="plain" ;;
                        "\\") escaped="true" ;;
                    esac
                    ;;
                plain)
                    case "$char" in
                        "'") state="single" ;;
                        '"') state="double" ;;
                        "\\") escaped="true" ;;
                        '#')
                            if [[ $i -eq 0 || "$previous" == ' ' || "$previous" == $'\t' ]]; then
                                return 1
                            fi
                            ;;
                        '<')
                            [[ "$next" == '<' ]] || continue
                            j=$((i + 2))
                            if [[ "${line:j:1}" == '-' ]]; then
                                strip_tabs="true"
                                j=$((j + 1))
                            fi
                            while [[ "${line:j:1}" == ' ' || "${line:j:1}" == $'\t' ]]; do
                                j=$((j + 1))
                            done
                            quote="${line:j:1}"
                            if [[ "$quote" == "'" || "$quote" == '"' ]]; then
                                quoted="true"
                                j=$((j + 1))
                                while [[ $j -lt ${#line} && "${line:j:1}" != "$quote" ]]; do
                                    delimiter="${delimiter}${line:j:1}"
                                    j=$((j + 1))
                                done
                                [[ $j -lt ${#line} ]] || return 1
              
  • hooks/config-change-handler.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Claude Octopus ConfigChange Hook Handler
    # Triggered when Claude Code configuration changes (v2.1.49+)
    # Detects Octopus setting changes and writes reload signal for orchestrate.sh
    # v8.29.0: Expanded from fast-mode-only to full Octopus settings hot-reload
    
    set -euo pipefail
    # EXIT trap — emits diagnostic stderr ONLY when the hook exits non-zero, so
    # the Claude Code harness error "No stderr output" can never recur. EXIT (not
    # ERR) avoids over-firing on intermediate `grep -o`/`cmd | ...` inside $() that
    # the hook's logic already handles. See issue #313.
    _octo_hook_exit() { local c=$?; if [[ $c -ne 0 ]]; then echo "[hook:$(basename "$0")] exit $c" >&2 2>/dev/null || true; fi; return 0; }
    trap _octo_hook_exit EXIT
    
    
    CONFIG_CHANGE_DATA=""
    if [[ ! -t 0 ]]; then
        CONFIG_CHANGE_DATA="$(cat)"
    fi
    
    SESSION_ID="${CLAUDE_SESSION_ID:-}"
    WORKFLOW_PHASE="${OCTOPUS_WORKFLOW_PHASE:-unknown}"
    SIGNAL_DIR="${HOME}/.claude-octopus"
    
    # Log the change for debugging
    if [[ "${VERBOSE:-false}" == "true" ]]; then
        echo "[ConfigChange] Session: $SESSION_ID, Phase: $WORKFLOW_PHASE" >&2
        if [[ -n "$CONFIG_CHANGE_DATA" ]]; then
            echo "[ConfigChange] Data: $CONFIG_CHANGE_DATA" >&2
        fi
    fi
    
    if [[ -n "$CONFIG_CHANGE_DATA" ]]; then
        # Detect fast mode toggle
        if echo "$CONFIG_CHANGE_DATA" | grep -q '"fast"' 2>/dev/null; then
            if [[ "${VERBOSE:-false}" == "true" ]]; then
                echo "[ConfigChange] Fast mode setting changed" >&2
            fi
        fi
    
        # Detect Octopus-specific setting changes
        NEEDS_RELOAD=false
        for setting in OCTOPUS_ROUTING_MODE OCTOPUS_AUTONOMY OCTOPUS_OPUS_MODE \
                       OCTOPUS_MAX_COST_USD OCTOPUS_MAX_PARALLEL_AGENTS \
                       OCTOPUS_QUALITY_GATE_THRESHOLD OCTOPUS_WORKTREE_ISOLATION \
                       OCTOPUS_WEBHOOK_URL OCTOPUS_AGY_SANDBOX OCTOPUS_CODEX_SANDBOX \
                       OCTOPUS_MEMORY_INJECTION OCTOPUS_PERSONA_PACKS OCTOPUS_COST_WARNINGS \
                       OCTOPUS_TOOL_POLICIES; do
            if echo "$CONFIG_CHANGE_DATA" | grep -q "\"$setting\"" 2>/dev/null; then
                NEEDS_RELOAD=true
                if [[ "${VERBOSE:-false}" == "true" ]]; then
                    echo "[ConfigChange] Octopus setting changed: $setting" >&2
                fi
            fi
        done
    
        # Write reload signal for orchestrate.sh to pick up on next invocation
        if [[ "$NEEDS_RELOAD" == "true" ]]; then
            mkdir -p "$SIGNAL_DIR"
            echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SIGNAL_DIR/.config-reload-signal" 2>/dev/null || true
            if [[ "${VERBOSE:-false}" == "true" ]]; then
                echo "[ConfigChange] Wrote reload signal for orchestrate.sh" >&2
            fi
        fi
    fi
    
    : # pass-through — current hook schema treats silence as continue
    exit 0
    
  • hooks/context-awareness.shGitHub
  • hooks/context-reinforcement.shRunsGitHub
    Read the script
    #!/bin/bash
    # Context Reinforcement Hook — SessionStart
    # Re-injects Iron Laws after context compaction so enforcement rules survive
    # conversation compression. Inspired by obra/superpowers v4.3.1 SessionStart pattern.
    #
    # Hook type: SessionStart
    # Returns: {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"<CONTEXT-REINFORCEMENT>...</CONTEXT-REINFORCEMENT>"}
    
    set -euo pipefail
    # EXIT trap — emits diagnostic stderr ONLY when the hook exits non-zero, so
    # the Claude Code harness error "No stderr output" can never recur. EXIT (not
    # ERR) avoids over-firing on intermediate `grep -o`/`cmd | ...` inside $() that
    # the hook's logic already handles. See issue #313.
    _octo_hook_exit() { local c=$?; if [[ $c -ne 0 ]]; then echo "[hook:$(basename "$0")] exit $c" >&2 2>/dev/null || true; fi; return 0; }
    trap _octo_hook_exit EXIT
    
    
    # Read JSON payload from stdin (required by hook protocol)
    if command -v timeout &>/dev/null; then
        INPUT=$(timeout 3 cat 2>/dev/null || true)
    else
        INPUT=$(cat 2>/dev/null || true)
    fi
    [[ -z "$INPUT" ]] && INPUT='{}'
    
    ACTIVATION_LIB="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}/scripts/lib/hook-activation.sh"
    [[ -r "$ACTIVATION_LIB" ]] || exit 0
    # shellcheck source=../scripts/lib/hook-activation.sh
    source "$ACTIVATION_LIB" 2>/dev/null || exit 0
    octo_hook_profile_allows "context-reinforcement" || exit 0
    octo_hook_workflow_active "$INPUT" || exit 0
    
    # Build compact enforcement context only for the active, matching workflow.
    # Native disable-model-invocation frontmatter is the hard activation gate; this
    # reminder only helps an explicitly started workflow survive compaction.
    read -r -d '' CONTEXT <<'RULES' || true
    <CONTEXT-REINFORCEMENT source="🐙 Octopus">
    Hard gates: no-stubs (verify before claiming done), test-first (failing test before code), debug-protocol (root cause before fix), orchestrate-only (use orchestrate.sh for research), factory-pipeline (no skipping steps).
    All Octopus commands and skills are explicit-only. Continue only the active workflow recorded for this Claude session.
    </CONTEXT-REINFORCEMENT>
    RULES
    
    # Escape the context for JSON output
    ESCAPED_CONTEXT=$(echo "$CONTEXT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))" 2>/dev/null | sed 's/^"//;s/"$//')
    
    # Return the hook response
    cat <<EOF
    {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"${ESCAPED_CONTEXT}"}}
    EOF
    
  • hooks/cwd-changed.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # cwd-changed.sh — Re-detect project context when working directory changes
    # Hook event: CwdChanged (CC v2.1.83+)
    # Outputs additionalContext with project type detection for the new directory.
    
    set -euo pipefail
    # EXIT trap — emits diagnostic stderr ONLY when the hook exits non-zero, so
    # the Claude Code harness error "No stderr output" can never recur. EXIT (not
    # ERR) avoids over-firing on intermediate `grep -o`/`cmd | ...` inside $() that
    # the hook's logic already handles. See issue #313.
    _octo_hook_exit() { local c=$?; if [[ $c -ne 0 ]]; then echo "[hook:$(basename "$0")] exit $c" >&2 2>/dev/null || true; fi; return 0; }
    trap _octo_hook_exit EXIT
    
    
    # Read hook input from stdin (JSON with new_cwd field)
    INPUT=$(cat 2>/dev/null) || INPUT=""
    [[ -z "$INPUT" ]] && exit 0
    
    # Extract new CWD — try jq first, fall back to regex
    if command -v jq &>/dev/null; then
        NEW_CWD=$(echo "$INPUT" | jq -r '.new_cwd // empty' 2>/dev/null) || NEW_CWD=""
    else
        # Fallback: extract with grep
        NEW_CWD=$(echo "$INPUT" | grep -o '"new_cwd"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"new_cwd"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//' 2>/dev/null) || NEW_CWD=""
    fi
    
    [[ -z "$NEW_CWD" || ! -d "$NEW_CWD" ]] && exit 0
    
    # Detect project characteristics
    context_hints=""
    
    # Check if it's a git repo
    if [[ -d "$NEW_CWD/.git" ]]; then
        context_hints="git-repo"
    fi
    
    # Detect language/framework
    if [[ -f "$NEW_CWD/package.json" ]]; then
        context_hints="${context_hints:+$context_hints, }node/js"
        if grep -q '"next"' "$NEW_CWD/package.json" 2>/dev/null; then
            context_hints="$context_hints, nextjs"
        fi
        if grep -q '"react"' "$NEW_CWD/package.json" 2>/dev/null; then
            context_hints="$context_hints, react"
        fi
    elif [[ -f "$NEW_CWD/pyproject.toml" || -f "$NEW_CWD/setup.py" || -f "$NEW_CWD/requirements.txt" ]]; then
        context_hints="${context_hints:+$context_hints, }python"
    elif [[ -f "$NEW_CWD/go.mod" ]]; then
        context_hints="${context_hints:+$context_hints, }go"
    elif [[ -f "$NEW_CWD/Cargo.toml" ]]; then
        context_hints="${context_hints:+$context_hints, }rust"
    fi
    
    # Detect if it has Claude config
    if [[ -d "$NEW_CWD/.claude" ]]; then
        context_hints="${context_hints:+$context_hints, }claude-configured"
    fi
    
    # Output context hint if we detected anything useful
    if [[ -n "$context_hints" ]]; then
        echo "[octopus] Directory changed to: $NEW_CWD (detected: $context_hints)"
    fi
    
    exit 0
    
  • hooks/discipline-inject.shRunsGitHub
  • hooks/discipline-task-gate.shRunsGitHub
  • hooks/done-criteria.shRunsGitHub
  • hooks/elicitation-handler.shRunsGitHub
  • hooks/fable5-inject.shRunsGitHub
  • hooks/freeze-check.shRunsGitHub
  • hooks/frontend-gate.shGitHub
  • hooks/github-work-queue-watch.shRunsGitHub
  • hooks/instructions-loaded.shRunsGitHub
  • hooks/octopus-hud.mjsGitHub
  • hooks/octopus-statusline.shGitHub
  • hooks/output-compressor.shGitHub
  • hooks/perf-gate.shGitHub
  • hooks/permission-denied-log.shRunsGitHub
  • hooks/plan-mode-interceptor.shRunsGitHub
  • hooks/plugin-update-advisory.shRunsGitHub
  • hooks/post-compact.shRunsGitHub
  • hooks/post-tool-dispatch.shRunsGitHub
  • hooks/pre-compact.shRunsGitHub
  • hooks/provider-routing-validator.shRunsGitHub
  • hooks/quality-gate.shRunsGitHub
  • hooks/safety-contract.pyGitHub
  • hooks/scheduler-security-gate.shRunsGitHub
  • hooks/security-gate.shGitHub
  • hooks/session-end.shRunsGitHub
  • hooks/session-start-memory.shRunsGitHub
  • hooks/statusline-auto-repair.shRunsGitHub
  • hooks/statusline-resolver.shGitHub
  • hooks/stop-failure-log.shRunsGitHub
  • hooks/strategy-rotation.shGitHub
  • hooks/subagent-result-capture.shRunsGitHub
  • hooks/subagent-stop-gate.shRunsGitHub
  • hooks/task-completed-transition.shRunsGitHub
  • hooks/task-completion-checkpoint.shRunsGitHub
  • hooks/task-dependency-validator.shRunsGitHub
  • hooks/teammate-idle-dispatch.shRunsGitHub
  • hooks/telemetry-webhook.shRunsGitHub
  • hooks/user-prompt-submit.shRunsGitHub
  • hooks/version-advisory.shRunsGitHub
  • hooks/workflow-verification.shRunsGitHub

All 51 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 withocto

Every AI model has blind spots. Claude Octopus supports twelve external provider integrations — Codex, Antigravity CLI, Copilot, Qwen, Ollama, Perplexity, OpenRouter, OrcaRouter, OpenCode, Cursor CLI, Grok, and Kimi Code — alongside the built-in Claude Code

Get the whole plugin