#!/usr/bin/env bash
# cmux claude wrapper - injects hooks and session tracking
#
# When running inside a cmux terminal (CMUX_SURFACE_ID is set), this wrapper
# intercepts `claude` invocations to inject --session-id and --settings flags
# so that Claude Code hooks fire back into cmux for notifications/status.
# Outside cmux, it passes through to the real claude binary unchanged.

# Every [[ == ]] below is an exact comparison (restore tokens, the generated
# settings fast path); an inherited nocasematch (BASH_ENV, BASHOPTS) must not
# make them case-insensitive.
shopt -u nocasematch 2>/dev/null || true

cmux_claude_wrapper_is_self_or_shim() {
    local candidate="$1"
    [[ -n "$candidate" ]] || return 1
    if [[ -e "$candidate" && "$candidate" -ef "$0" ]]; then
        return 0
    fi
    if [[ -n "${CMUX_CLAUDE_WRAPPER_SHIM:-}" &&
          -e "$candidate" &&
          -e "$CMUX_CLAUDE_WRAPPER_SHIM" &&
          "$candidate" -ef "$CMUX_CLAUDE_WRAPPER_SHIM" ]]; then
        return 0
    fi
    if [[ -n "${CMUX_CLAUDE_WRAPPER_SHIM_ROOT:-}" &&
          "$candidate" == "${CMUX_CLAUDE_WRAPPER_SHIM_ROOT%/}/claude" ]]; then
        return 0
    fi
    case "$candidate" in
        */cmux-cli-shims/*/claude|*/cmux-cli-shims/claude)
            return 0
            ;;
        */Contents/Resources/bin/claude|*/Resources/bin/claude)
            return 0
            ;;
    esac
    return 1
}

cmux_claude_wrapper_reexec_guard_limit=16
cmux_claude_wrapper_reexec_hops=0
# Newline-delimited, not PATH/colon-delimited: custom executable paths can
# legally contain ':' even though PATH entries cannot.
cmux_claude_wrapper_reexec_target_history=""

# Keep argument inspection bounded. Apart from protecting the shell's pattern
# matcher, this prevents a caller from turning the wrapper's own launch metadata
# into another ARG_MAX-sized environment value.
# Stay below Linux's 128 KiB per-argument exec limit so the same guard can be
# exercised portably; larger settings should be supplied by file path.
cmux_claude_wrapper_max_argument_bytes=122880

cmux_claude_wrapper_validate_arguments() {
    local arg
    local byte_length
    local had_lc_all=0
    local previous_lc_all="${LC_ALL-}"
    local too_large=0
    if [[ ${LC_ALL+x} ]]; then
        had_lc_all=1
    fi
    export LC_ALL=C
    for arg in "$@"; do
        byte_length=${#arg}
        if (( byte_length > cmux_claude_wrapper_max_argument_bytes )); then
            too_large=1
            break
        fi
    done
    if (( had_lc_all == 1 )); then
        export LC_ALL="$previous_lc_all"
    else
        unset LC_ALL
    fi
    if (( too_large == 1 )); then
        printf 'cmux: argument too large (maximum %s bytes)\n' \
            "$cmux_claude_wrapper_max_argument_bytes" >&2
        return 2
    fi
    return 0
}

cmux_claude_wrapper_settings_value_is_managed() {
    local value="$1"
    local trimmed="${value#"${value%%[![:space:]]*}"}"
    if [[ "$trimmed" == \{* || "$trimmed" == \[* ]]; then
        case "$trimmed" in
            *'"__cmux":{"managed":"claude-hooks"'*|*'hooks feed --source claude'*)
                return 0
                ;;
        esac
        return 1
    fi
    [[ -f "$trimmed" ]] || return 1
    grep -Fq '"__cmux":{"managed":"claude-hooks"' "$trimmed" 2>/dev/null && return 0
    grep -Fq 'hooks feed --source claude' "$trimmed" 2>/dev/null
}

cmux_claude_wrapper_args_have_managed_settings() {
    local collect=false
    local arg
    for arg in "$@"; do
        if [[ "$collect" == true ]]; then
            cmux_claude_wrapper_settings_value_is_managed "$arg" && return 0
            collect=false
            continue
        fi
        if [[ "$arg" == "--settings" ]]; then
            collect=true
        elif [[ "${arg:0:11}" == "--settings=" ]]; then
            cmux_claude_wrapper_settings_value_is_managed "${arg#--settings=}" && return 0
        fi
    done
    return 1
}

cmux_claude_wrapper_path_without_shims() {
    local current_path="${PATH:-}"
    local shim_root="${CMUX_CLAUDE_WRAPPER_SHIM_ROOT:-}"
    local old_ifs="$IFS"
    local entry
    local result=""
    local globbing_was_disabled=0

    case "$-" in
        *f*) globbing_was_disabled=1 ;;
        *) set -f ;;
    esac
    IFS=:
    for entry in ${current_path:-}; do
        if [[ "$entry" == "$shim_root" ||
              "$entry" == */cmux-cli-shims/* ||
              "$entry" == */cmux-cli-shims ]]; then
            continue
        fi
        if [[ -z "$result" ]]; then
            result="$entry"
        else
            result="$result:$entry"
        fi
    done
    IFS="$old_ifs"
    if (( globbing_was_disabled == 0 )); then
        set +f
    fi
    printf '%s' "$result"
}

cmux_claude_wrapper_target_is_custom_path() {
    local target="$1"
    local custom="${CMUX_CUSTOM_CLAUDE_PATH:-}"
    custom="${custom#"${custom%%[![:space:]]*}"}"
    custom="${custom%"${custom##*[![:space:]]}"}"
    [[ -n "$custom" && -n "$target" ]] || return 1
    [[ "$target" == "$custom" ]] && return 0
    [[ -e "$target" && -e "$custom" && "$target" -ef "$custom" ]]
}

cmux_claude_wrapper_prepare_custom_path_environment() {
    local target="$1"
    [[ "${IN_CMUX:-0}" == "1" ]] || return 0
    cmux_claude_wrapper_target_is_custom_path "$target" || return 0
    local cleaned_path
    if cleaned_path="$(cmux_claude_wrapper_path_without_shims)" && [[ -n "$cleaned_path" ]]; then
        export PATH="$cleaned_path"
    fi
}

cmux_claude_wrapper_is_nonnegative_integer() {
    case "$1" in
        ''|*[!0-9]*)
            return 1
            ;;
    esac
    return 0
}

cmux_claude_wrapper_fail_reexec_guard() {
    cat >&2 <<'EOF'
cmux: stopped a possible infinite claude shim loop.
cmux's per-surface claude shim re-entered before the real binary started.
This is usually caused by a conflicting `claude` shim from another tool on PATH.
Set CMUX_CUSTOM_CLAUDE_PATH, or set Automation > Claude Binary Path to the real
claude binary, then start a new surface.
EOF
    exit 126
}

cmux_claude_wrapper_init_reexec_guard() {
    local guard="${cmux_claude_wrapper_reexec_guard:-}"
    local hops

    case "$guard" in
        *[!0-9]*|'')
            unset cmux_claude_wrapper_reexec_guard
            unset cmux_claude_wrapper_reexec_targets
            cmux_claude_wrapper_reexec_hops=0
            cmux_claude_wrapper_reexec_target_history=""
            ;;
        *)
            hops="$guard"
            cmux_claude_wrapper_is_nonnegative_integer "$hops" || hops=0
            cmux_claude_wrapper_reexec_hops="$hops"
            cmux_claude_wrapper_reexec_target_history="${cmux_claude_wrapper_reexec_targets:-}"
            ;;
    esac
}

cmux_claude_wrapper_reexec_target_seen() {
    local target="$1"
    local seen
    while IFS= read -r seen; do
        [[ "$seen" == "$target" ]] && return 0
    done <<< "$cmux_claude_wrapper_reexec_target_history"
    return 1
}

cmux_claude_wrapper_custom_target_is_direct_absolute_claude() {
    local snippet="$1"
    local line rest token
    while IFS= read -r line; do
        case "$line" in
            *exec\ *)
                rest="${line#*exec }"
                rest="${rest#\"}"
                rest="${rest#\'}"
                token="${rest%%[[:space:]]*}"
                token="${token%\"}"
                token="${token%\'}"
                if [[ "$token" == /*/claude || "$token" == /claude ]] &&
                   [[ -e "$token" && -x "$token" ]] &&
                   ! cmux_claude_wrapper_is_self_or_shim "$token"; then
                    return 0
                fi
                ;;
        esac
    done <<< "$snippet"
    return 1
}

cmux_claude_wrapper_target_looks_like_reexec_shim() {
    local target="$1"
    cmux_claude_wrapper_is_self_or_shim "$target" && return 0
    [[ -f "$target" && -r "$target" ]] || return 1

    case "$target" in
        */.asdf/shims/claude|*/.mise/shims/claude|*/.local/share/mise/shims/claude|*/.volta/bin/claude)
            return 1
            ;;
    esac

    local magic
    magic="$(od -An -N2 -tx1 "$target" 2>/dev/null | tr -d '[:space:]')"
    [[ "$magic" == "2321" ]] || return 1

    local first_line snippet
    IFS= read -r first_line < "$target" || first_line=""
    snippet="$(sed -n '1,160p' "$target" 2>/dev/null || true)"

    case "$snippet" in
        *"exec claude"*|*"exec \"claude\""*|*"exec 'claude'"*|\
        *"exec env claude"*|*"exec /usr/bin/env claude"*|\
        *"env claude"*|*"env \"claude\""*|*"env 'claude'"*|\
        *"command claude"*|*"command \"claude\""*|*"command 'claude'"*|\
        *"spawn(\"claude\""*|*"spawn('claude'"*|\
        *"spawnSync(\"claude\""*|*"spawnSync('claude'"*|\
        *"execFile(\"claude\""*|*"execFile('claude'"*|\
        *"execFileSync(\"claude\""*|*"execFileSync('claude'"*|\
        *"execSync(\"claude"*|*"execSync('claude"*|\
        *"os.execvp(\"claude\""*|*"os.execvp('claude'"*|\
        *"os.execlp(\"claude\""*|*"os.execlp('claude'"*|\
        *"subprocess.run([\"claude\""*|*"subprocess.run(['claude'"*|\
        *"subprocess.call([\"claude\""*|*"subprocess.call(['claude'"*|\
        *"subprocess.Popen([\"claude\""*|*"subprocess.Popen(['claude'"*)
            return 0
            ;;
    esac
    case "$snippet" in
        *'=claude'*|*'="claude"'*|*"='claude'"*|\
        *':-claude}'*|*':-"claude"}'*|*":-'claude'}"*)
            case "$snippet" in
                *'exec "$'*|*'exec ${'*)
                    return 0
                    ;;
            esac
            ;;
    esac
    case "$snippet" in
        *'@anthropic-ai/claude-code'*|*'claude-code/cli.js'*)
            return 1
            ;;
    esac
    local target_is_custom=false
    if [[ "${IN_CMUX:-0}" == "1" ]] && cmux_claude_wrapper_target_is_custom_path "$target"; then
        target_is_custom=true
    fi
    if [[ "$target_is_custom" != true ]]; then
        case "$first_line" in
            *node*|*Node*)
                return 1
                ;;
        esac
    fi

    # A configured launcher can delegate through another executable (for
    # example `exec some-launcher claude "$@"`) without matching the finite
    # command forms above. Treat a bare Claude token in that launcher as a
    # possible re-entry, but keep direct absolute `/.../claude` launchers on
    # the real-binary side of the boundary.
    if [[ "$target_is_custom" == true ]]; then
        cmux_claude_wrapper_custom_target_is_direct_absolute_claude "$snippet" && return 1
        local launcher_line
        while IFS= read -r launcher_line; do
            if [[ "$launcher_line" == *'exec "'*'/claude" '* ||
                  "$launcher_line" == *'exec '*/claude' '* ]]; then
                continue
            fi
            case "$launcher_line" in
                *'exec '*claude*|*'spawn'*claude*|*'which'*claude*|*'command -v '*claude*)
                    return 0
                    ;;
            esac
        done <<< "$snippet"

        # A configured script is a launcher boundary unless it is an explicit
        # absolute Claude executable. Keep the re-entry counter across generic
        # launcher syntax (`exec "$LAUNCHER"`, `env`, etc.) so an indirect PATH
        # lookup cannot reset it between wrapper passes.
        case "$snippet" in
            *'exec '*|*'command '*|*'spawn'*|*'which '*)
                return 0
                ;;
        esac
    fi
    return 1
}

cmux_claude_wrapper_exec_real_after_managed_reentry() {
    local previous_custom_path="${CMUX_CUSTOM_CLAUDE_PATH:-}"
    unset CMUX_CUSTOM_CLAUDE_PATH
    local fallback
    fallback="$(find_real_claude)"
    if [[ -n "$previous_custom_path" ]]; then
        export CMUX_CUSTOM_CLAUDE_PATH="$previous_custom_path"
    fi
    [[ -n "$fallback" ]] || cmux_claude_wrapper_fail_reexec_guard
    cmux_claude_wrapper_target_looks_like_reexec_shim "$fallback" &&
        cmux_claude_wrapper_fail_reexec_guard
    unset CMUX_AGENT_RESTORE_LAUNCH
    unset CMUX_CLAUDE_TEAMS_WRAPPER_LAUNCH
    unset cmux_claude_wrapper_reexec_guard
    unset cmux_claude_wrapper_reexec_targets
    exec "$fallback" "$@"
}

cmux_claude_wrapper_exec_resolved_claude() {
    local target="$1"
    shift

    if ! cmux_claude_wrapper_target_looks_like_reexec_shim "$target"; then
        # A non-shim target is the boundary where a real Claude process starts.
        # Custom launcher scripts that can delegate to Claude are classified as
        # re-entry shims above, so this reset is only reached for a proven real
        # boundary. Real Claude children that invoke `claude` later can therefore
        # begin a fresh wrapper resolution without inheriting the hop counter.
        unset CMUX_AGENT_RESTORE_LAUNCH
        unset CMUX_CLAUDE_TEAMS_WRAPPER_LAUNCH
        unset cmux_claude_wrapper_reexec_guard
        unset cmux_claude_wrapper_reexec_targets
        # A configured Claude path is often a launcher script rather than the
        # Claude executable itself. It must not inherit cmux's PATH shim: a
        # downstream `which`/execvp/command -v lookup would otherwise route back
        # into this wrapper after the guard was cleared.
        cmux_claude_wrapper_prepare_custom_path_environment "$target"
        exec "$target" "$@"
    fi
    if cmux_claude_wrapper_reexec_target_seen "$target"; then
        if cmux_claude_wrapper_args_have_managed_settings "$@"; then
            cmux_claude_wrapper_exec_real_after_managed_reentry "$@"
        fi
        cmux_claude_wrapper_fail_reexec_guard "$target"
    fi
    if (( cmux_claude_wrapper_reexec_hops >= cmux_claude_wrapper_reexec_guard_limit )); then
        cmux_claude_wrapper_fail_reexec_guard
    fi

    local hops="${cmux_claude_wrapper_reexec_hops:-0}"
    cmux_claude_wrapper_is_nonnegative_integer "$hops" || hops=0
    hops=$((hops + 1))
    export cmux_claude_wrapper_reexec_guard="$hops"
    if [[ -n "$cmux_claude_wrapper_reexec_target_history" ]]; then
        cmux_claude_wrapper_reexec_target_history="$cmux_claude_wrapper_reexec_target_history"$'\n'"$target"
    else
        cmux_claude_wrapper_reexec_target_history="$target"
    fi
    cmux_claude_wrapper_reexec_targets="$cmux_claude_wrapper_reexec_target_history"
    export cmux_claude_wrapper_reexec_targets
    if [[ "${cmux_claude_app_restore_owned:-0}" == "1" ]]; then
        # A supported shim will invoke this wrapper again. Re-issue the exact
        # authorization for that pass; the next wrapper consumes it immediately.
        export CMUX_AGENT_RESTORE_LAUNCH="$cmux_claude_app_restore_launch"
    fi
    if [[ "${cmux_claude_teams_wrapper_launch:-0}" == "1" ]]; then
        # Keep the one-shot launch signal across finite version-manager shim
        # hops. The final real-Claude boundary above consumes it.
        export CMUX_CLAUDE_TEAMS_WRAPPER_LAUNCH=1
    fi
    cmux_claude_wrapper_prepare_custom_path_environment "$target"
    exec "$target" "$@"
}

cmux_claude_wrapper_init_reexec_guard

# `cmux claude-teams` resolves the real provider itself, then deliberately
# enters this app-owned wrapper so hooks can bind the lead session. Preserve its
# structured launcher capture for that one process only. The marker is consumed
# before Claude starts, so teammate `claude` commands inherit no launch signal
# and receive their own ordinary Claude capture below.
cmux_claude_teams_wrapper_launch=0
if [[ "${CMUX_CLAUDE_TEAMS_WRAPPER_LAUNCH:-}" == "1" &&
      "${CMUX_AGENT_LAUNCH_KIND:-}" == "claudeTeams" &&
      -n "${CMUX_AGENT_LAUNCH_EXECUTABLE:-}" &&
      "${CMUX_AGENT_LAUNCH_EXECUTABLE:-}" == "${CMUX_CLAUDE_TEAMS_CMUX_BIN:-}" &&
      -n "${CMUX_AGENT_LAUNCH_ARGV_B64:-}" ]]; then
    cmux_claude_teams_wrapper_launch=1
fi
unset CMUX_CLAUDE_TEAMS_WRAPPER_LAUNCH

# Find the real claude binary, skipping cmux's wrapper and temp shell shims.
find_real_claude() {
    # Honor custom path from Settings > Automation > Claude Code > Claude Binary Path.
    # Guard against self-reference (user pointing back to this wrapper).
    local custom="${CMUX_CUSTOM_CLAUDE_PATH:-}"
    custom="${custom#"${custom%%[![:space:]]*}"}"  # trim leading whitespace
    custom="${custom%"${custom##*[![:space:]]}"}"  # trim trailing whitespace
    if [[ -n "$custom" && -f "$custom" && -x "$custom" ]]; then
        # Use -ef to detect same underlying file even through symlinks.
        if ! cmux_claude_wrapper_is_self_or_shim "$custom"; then
            printf '%s' "$custom"
            return 0
        fi
    fi
    local self_dir
    self_dir="$(cd "$(dirname "$0")" && pwd)"
    local IFS=:
    for d in $PATH; do
        [[ "$d" == "$self_dir" ]] && continue
        local candidate="$d/claude"
        cmux_claude_wrapper_is_self_or_shim "$candidate" && continue
        [[ -x "$candidate" ]] && printf '%s' "$candidate" && return 0
    done
    return 1
}

# Return 0 only when CMUX_SOCKET_PATH points to a live cmux socket.
cmux_socket_available() {
    local socket="${CMUX_SOCKET_PATH:-}"
    [[ -n "$socket" && -S "$socket" ]] || return 1

    local self_dir cmux_bin
    self_dir="$(cd "$(dirname "$0")" && pwd)"
    cmux_bin="$self_dir/cmux"
    [[ -x "$cmux_bin" ]] || cmux_bin="$(command -v cmux || true)"
    [[ -n "$cmux_bin" ]] || return 1

    # Keep stale/hung socket checks bounded so claude startup does not block
    # behind the CLI default timeout (15s).
    CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.75 \
        "$cmux_bin" --socket "$socket" ping >/dev/null 2>&1
}

resolve_hook_cmux_bin() {
    local self_dir bundled_cli
    bundled_cli="${CMUX_BUNDLED_CLI_PATH:-}"
    if [[ -n "$bundled_cli" && -x "$bundled_cli" ]]; then
        printf '%s' "$bundled_cli"
        return 0
    fi

    self_dir="$(cd "$(dirname "$0")" && pwd)"
    bundled_cli="$self_dir/cmux"
    if [[ -x "$bundled_cli" ]]; then
        printf '%s' "$bundled_cli"
        return 0
    fi

    bundled_cli="$(command -v cmux || true)"
    if [[ -n "$bundled_cli" ]]; then
        printf '%s' "$bundled_cli"
        return 0
    fi

    printf '%s' "cmux"
}

CLAUDE_AUTH_SELECTION_ENV_KEYS=(
    ANTHROPIC_API_KEY
    ANTHROPIC_MODEL
    ANTHROPIC_SMALL_FAST_MODEL
    CLAUDE_CODE_USE_BEDROCK
    CLAUDE_CODE_USE_VERTEX
)

PRESERVE_CLAUDE_AUTH_SELECTION_ENV=false
case "${CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV:-}" in
    1|true|TRUE|yes|YES) PRESERVE_CLAUDE_AUTH_SELECTION_ENV=true ;;
esac

# Return 0 when an ANTHROPIC_MODEL / ANTHROPIC_SMALL_FAST_MODEL value carries a
# Vertex/Bedrock-specific shape that is only valid against those backends and
# would mis-route (or be rejected) on the default Anthropic API:
#   - Vertex pins the publisher date with '@', e.g. claude-sonnet-4-5@20250929
#   - Bedrock uses the dotted vendor namespace + version/profile suffix, e.g.
#     us.anthropic.claude-sonnet-4-5-20250929-v1:0 (and regional eu./apac./
#     us-gov. variants), bare anthropic.<model>-v1:0, or an arn:aws:bedrock ARN.
# Plain ids such as claude-opus-4-8 or claude-opus-4-8[1m] carry none of these
# markers and are valid on the default Anthropic path, so they are NOT
# backend-qualified. https://github.com/manaflow-ai/cmux/issues/7047
claude_model_id_is_backend_qualified() {
    case "$1" in
        *@*|*:*|*/*) return 0 ;;
        *anthropic.*) return 0 ;;
    esac
    return 1
}

should_preserve_claude_auth_selection_key() {
    if [[ "$PRESERVE_CLAUDE_AUTH_SELECTION_ENV" == true ]]; then
        local configured_keys="${CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS:-}"
        if [[ -z "$configured_keys" ]]; then
            return 0
        fi
        configured_keys="${configured_keys//,/ }"
        local configured_key
        for configured_key in $configured_keys; do
            [[ "$configured_key" == "$1" ]] && return 0
        done
        # Key not in the explicit list: do NOT early-return — fall through to
        # the Vertex/Bedrock auto-preserve below. CMUX_PRESERVE_CLAUDE_AUTH_SELECTION_ENV_KEYS
        # is an additive allow-list, not an exclusion list, so an operator
        # who opts in with a narrower key list still gets Vertex/Bedrock
        # selection signals preserved when those backends are active.
    fi

    # Auto-preserve Vertex/Bedrock auth selection so users with valid
    # CLAUDE_CODE_USE_VERTEX=1 / CLAUDE_CODE_USE_BEDROCK=1 setups in their
    # shell rc authenticate inside cmux the same way they do in plain
    # Terminal. Without this, the wrapper silently drops the selection
    # signal and claude reports "not logged in".
    # Refs: https://github.com/manaflow-ai/cmux/issues/3641 (Vertex)
    #       https://github.com/manaflow-ai/cmux/issues/3638 (Bedrock)
    local vertex_on=false bedrock_on=false
    case "${CLAUDE_CODE_USE_VERTEX:-}" in
        1|true|TRUE|yes|YES) vertex_on=true ;;
    esac
    case "${CLAUDE_CODE_USE_BEDROCK:-}" in
        1|true|TRUE|yes|YES) bedrock_on=true ;;
    esac

    case "$1" in
        CLAUDE_CODE_USE_VERTEX)
            [[ "$vertex_on" == true ]] && return 0
            ;;
        CLAUDE_CODE_USE_BEDROCK)
            [[ "$bedrock_on" == true ]] && return 0
            ;;
        ANTHROPIC_MODEL|ANTHROPIC_SMALL_FAST_MODEL)
            # Vertex and Bedrock require backend-specific full model ids
            # (e.g. "claude-sonnet-4-5@20250929" for Vertex,
            # "us.anthropic.claude-sonnet-4-5-20250929-v1:0" for Bedrock).
            # Preserve the user's selection when one of those backends is
            # active.
            if [[ "$vertex_on" == true || "$bedrock_on" == true ]]; then
                return 0
            fi
            # Default Anthropic API path (no Vertex/Bedrock backend active):
            # preserve a user-pinned PLAIN model id (e.g. "claude-opus-4-8[1m]")
            # so cmux behaves like a plain Terminal and a Max-plan 1M context
            # window selection survives every new pane/reboot. Still strip
            # backend-qualified ids: a stale "...@<date>" / "...anthropic...:..."
            # value is invalid on the Anthropic path and is exactly the leak the
            # scrub guards against. https://github.com/manaflow-ai/cmux/issues/7047
            local model_key="$1"
            local model_value="${!model_key-}"
            if [[ -n "$model_value" ]] &&
               ! claude_model_id_is_backend_qualified "$model_value"; then
                return 0
            fi
            ;;
    esac

    return 1
}

normalize_claude_config_dir() {
    [[ -n "${CLAUDE_CONFIG_DIR:-}" ]] || return 0
    [[ -n "${HOME:-}" ]] || return 0

    local value="$CLAUDE_CONFIG_DIR"
    local legacy_root="$HOME/.subrouter/codex/claude"
    local account_root="$HOME/.codex-accounts/claude"
    if [[ "$value" == "~/"* ]]; then
        value="$HOME/${value#~/}"
    fi

    case "$value" in
        "$legacy_root"|"$legacy_root"/*)
            local suffix="${value#$legacy_root}"
            local candidate="$account_root$suffix"
            if [[ -e "$candidate" || -L "$candidate" ]]; then
                export CLAUDE_CONFIG_DIR="$candidate"
            fi
            ;;
    esac
}

# Claude stores transcripts at <config-dir>/projects/<encode(cwd)>/<id>.jsonl and
# `--resume <id>` only resolves a session under the *current* CLAUDE_CONFIG_DIR.
# cmux can launch with a foreign CLAUDE_CONFIG_DIR (the .app inherits the env of
# whatever opened it, e.g. a terminal whose agent set one), so resuming a session
# created under a different config root fails with "No conversation found" and drops
# the user to a bare shell. Self-heal by relocating CLAUDE_CONFIG_DIR to the root
# that actually holds the transcript, but only when the current root lacks it (so a
# correct resume is never repointed). https://github.com/manaflow-ai/cmux/issues/6194
cmux_claude_config_dir_has_session() {
    local root="$1" sid="$2"
    [[ -n "$root" && -d "$root/projects" ]] || return 1
    local transcript
    for transcript in \
        "$root"/projects/*/"$sid".jsonl \
        "$root"/projects/*/*/"$sid".jsonl \
        "$root"/projects/*/*/*/"$sid".jsonl \
        "$root"/projects/*/*/*/*/"$sid".jsonl \
        "$root"/projects/*/*/*/*/*/"$sid".jsonl; do
        [[ -f "$transcript" && -s "$transcript" ]] && return 0
    done
    return 1
}

claude_option_consumes_value() {
    case "$1" in
        --add-dir|--agent|--agents|--allowedTools|--allowed-tools|\
        --append-system-prompt|--betas|--debug-file|--disallowedTools|\
        --disallowed-tools|--effort|--fallback-model|--file|\
        --input-format|--json-schema|--max-budget-usd|--mcp-config|\
        --model|-m|-n|--name|--output-format|--permission-mode|--plugin-dir|\
        --plugin-url|--remote-control-session-name-prefix|--setting-sources|\
        --settings|--system-prompt|--tools)
            return 0
            ;;
    esac
    return 1
}

claude_remaining_args_have_resume_flag() {
    local arg
    for arg in "$@"; do
        case "$arg" in
            --)
                return 1
                ;;
            --resume|-r|--resume=*)
                return 0
                ;;
        esac
    done
    return 1
}

claude_remaining_args_have_explicit_session_flag() {
    local arg
    for arg in "$@"; do
        case "$arg" in
            --)
                return 1
                ;;
            --resume|--resume=*|-r|--session-id|--session-id=*|--continue|-c)
                return 0
                ;;
        esac
    done
    return 1
}

claude_unknown_option_should_skip_value_before_resume_flag() {
    local index="$1"
    local -a args=("${@:2}")
    local next_index=$((index + 1))
    local rest_index=$((index + 2))
    (( next_index < ${#args[@]} )) || return 1
    [[ "${args[$next_index]}" != -* ]] || return 1
    claude_remaining_args_have_resume_flag "${args[@]:$rest_index}"
}

claude_unknown_option_should_skip_value_before_explicit_session_flag() {
    local index="$1"
    local -a args=("${@:2}")
    local next_index=$((index + 1))
    local rest_index=$((index + 2))
    (( next_index < ${#args[@]} )) || return 1
    [[ "${args[$next_index]}" != -* ]] || return 1
    claude_remaining_args_have_explicit_session_flag "${args[@]:$rest_index}"
}

extract_claude_resume_session_id() {
    local -a args=("$@")
    local index=0
    local arg
    while (( index < ${#args[@]} )); do
        arg="${args[$index]}"
        case "$arg" in
            --)
                return 1
                ;;
            --resume|-r)
                index=$((index + 1))
                (( index < ${#args[@]} )) || return 1
                printf '%s' "${args[$index]}"
                return 0
                ;;
            --resume=*)
                printf '%s' "${arg#--resume=}"
                return 0
                ;;
            -*)
                if [[ "$arg" != *=* ]]; then
                    if claude_option_consumes_value "$arg" ||
                       claude_unknown_option_should_skip_value_before_resume_flag "$index" "${args[@]}"; then
                        index=$((index + 2))
                        continue
                    fi
                fi
                ;;
            *)
                ;;
        esac
        index=$((index + 1))
    done
    return 1
}

claude_explicit_session_flag_present() {
    local -a args=("$@")
    local index=0
    local arg
    while (( index < ${#args[@]} )); do
        arg="${args[$index]}"
        case "$arg" in
            --)
                return 1
                ;;
            --resume|--resume=*|-r|--session-id|--session-id=*|--continue|-c)
                return 0
                ;;
            -*)
                if [[ "$arg" != *=* ]]; then
                    if claude_option_consumes_value "$arg" ||
                       claude_unknown_option_should_skip_value_before_explicit_session_flag "$index" "${args[@]}"; then
                        index=$((index + 2))
                        continue
                    fi
                fi
                ;;
            *)
                ;;
        esac
        index=$((index + 1))
    done
    return 1
}

reconcile_claude_config_dir_for_resume() {
    local sid="$1"
    [[ -n "$sid" && -n "${HOME:-}" ]] || return 0
    # Only safe filename tokens (claude session ids are uuids); never glob-walk on
    # path separators or dot segments.
    case "$sid" in
        *[!A-Za-z0-9._-]*|''|.|..) return 0 ;;
    esac

    local current_root="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
    cmux_claude_config_dir_has_session "$current_root" "$sid" && return 0

    local root
    for root in "$HOME/.claude" "$HOME"/.codex-accounts/claude/* "$HOME/.subrouter/codex/claude" "$HOME"/.subrouter/codex/claude/*; do
        [[ "$root" == "$current_root" ]] && continue
        if cmux_claude_config_dir_has_session "$root" "$sid"; then
            export CLAUDE_CONFIG_DIR="$root"
            return 0
        fi
    done
}

clear_inherited_claude_auth_selection_env() {
    if [[ "${IN_CMUX:-0}" == "1" ]]; then
        local key
        for key in "${CLAUDE_AUTH_SELECTION_ENV_KEYS[@]}"; do
            should_preserve_claude_auth_selection_key "$key" && continue
            unset "$key"
        done
    fi
    # Safe outside cmux: this only rewrites an existing subrouter path when the account alias exists.
    normalize_claude_config_dir
}

clear_inherited_claude_session_identity_env() {
    # Generated from scripts/claude-launch-environment-policy.json.
    # ClaudeWrapperResumeEnvironmentTests exercises every session identity key here.
    # BEGIN GENERATED CLAUDE LAUNCH ENVIRONMENT POLICY
    unset CLAUDECODE
    unset CLAUDE_CODE
    unset CLAUDE_CODE_CHILD_SESSION
    unset CLAUDE_CODE_BRIDGE_SESSION_ID
    unset CLAUDE_CODE_PARENT_SESSION_ID
    unset CLAUDE_CODE_SESSION_ID
    unset CLAUDE_CODE_ENTRYPOINT
    unset CLAUDE_CODE_EXECPATH
    unset CLAUDE_CODE_SSE_PORT
    # END GENERATED CLAUDE LAUNCH ENVIRONMENT POLICY
}

# `cmux hooks claude inject-settings` prints a fixed document and never
# touches the app socket, so it can run while the socket ping below decides
# whether this launch gets hooks at all. A background job runs the generator
# under its 1 s watchdog and writes its output to a private file; the wrapper
# either collects it (hook injection) or discards it (every passthrough). An
# unexpected `exit` in between discards it via the EXIT trap; exec does not run
# that trap.
unset cmux_claude_hook_settings_file cmux_claude_hook_settings_job_pid cmux_claude_hook_settings_value
cmux_claude_hook_settings_file=""
cmux_claude_hook_settings_job_pid=""
cmux_claude_hook_settings_value=""

# Background job body; stdout is the collection file. The generator and its
# watchdog are children of this job, so the job reaps the generator and stops
# the watchdog as soon as it exits, and TERM from a discard stops both.
cmux_claude_run_hook_settings_generator() {
    local hook_cmux_bin="$1" generator_pid="" watchdog_pid="" generator_status
    local term_requested=0
    # Record a TERM that lands while the children are being spawned; act on it
    # once both pids are known.
    trap 'term_requested=1' TERM

    CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.75 \
        "$hook_cmux_bin" hooks claude inject-settings 2>/dev/null &
    generator_pid=$!
    (
        cmux_generator_timer_pid=""
        trap 'if [[ -n "$cmux_generator_timer_pid" ]]; then kill "$cmux_generator_timer_pid" 2>/dev/null || true; fi' EXIT
        trap 'exit 0' HUP INT TERM
        /bin/sleep 1 &
        cmux_generator_timer_pid=$!
        wait "$cmux_generator_timer_pid" 2>/dev/null || exit 0
        cmux_generator_timer_pid=""
        trap - EXIT HUP INT TERM
        kill -KILL "$generator_pid" 2>/dev/null || true
    ) >/dev/null &
    watchdog_pid=$!
    trap 'kill -KILL "$generator_pid" 2>/dev/null; kill "$watchdog_pid" 2>/dev/null; exit 143' TERM
    if (( term_requested )); then
        kill -KILL "$generator_pid" 2>/dev/null
        kill "$watchdog_pid" 2>/dev/null
        exit 143
    fi

    wait "$generator_pid" 2>/dev/null
    generator_status=$?
    kill "$watchdog_pid" 2>/dev/null || true
    wait "$watchdog_pid" 2>/dev/null || true
    exit "$generator_status"
}

cmux_claude_start_hook_settings() {
    local hook_cmux_bin="$1"
    cmux_claude_hook_settings_file="$(mktemp "${TMPDIR:-/tmp}/cmux-claude-hook-settings.XXXXXX" 2>/dev/null)" || {
        cmux_claude_hook_settings_file=""
        return 1
    }
    trap cmux_claude_discard_hook_settings EXIT
    cmux_claude_run_hook_settings_generator "$hook_cmux_bin" >"$cmux_claude_hook_settings_file" &
    cmux_claude_hook_settings_job_pid=$!
}

# Waits for the job (at most until its watchdog fires) and sets
# cmux_claude_hook_settings_value to the generator's output. Returns the
# generator status, or 1 when the job could not be started.
cmux_claude_collect_hook_settings() {
    local hook_cmux_bin="$1"
    if [[ -z "$cmux_claude_hook_settings_job_pid" ]]; then
        cmux_claude_start_hook_settings "$hook_cmux_bin" || return 1
    fi
    cmux_claude_finish_hook_settings
}

cmux_claude_finish_hook_settings() {
    local generator_status=1
    cmux_claude_hook_settings_value=""
    if [[ -n "$cmux_claude_hook_settings_job_pid" ]]; then
        wait "$cmux_claude_hook_settings_job_pid" 2>/dev/null
        generator_status=$?
    fi
    if [[ $generator_status -eq 0 && -n "$cmux_claude_hook_settings_file" ]]; then
        cmux_claude_hook_settings_value="$(<"$cmux_claude_hook_settings_file")"
    fi
    if [[ -n "$cmux_claude_hook_settings_file" ]]; then
        rm -f -- "$cmux_claude_hook_settings_file"
    fi
    cmux_claude_hook_settings_file=""
    cmux_claude_hook_settings_job_pid=""
    trap - EXIT
    return "$generator_status"
}

cmux_claude_discard_hook_settings() {
    [[ -n "$cmux_claude_hook_settings_job_pid$cmux_claude_hook_settings_file" ]] || return 0
    # Signal the job only while bash still lists it as running: once it has
    # exited, bash has already reaped it and the pid may belong to someone else.
    local newline=$'\n'
    if [[ -n "$cmux_claude_hook_settings_job_pid" &&
          "$newline$(jobs -rp)$newline" == *"$newline$cmux_claude_hook_settings_job_pid$newline"* ]]; then
        kill -TERM "$cmux_claude_hook_settings_job_pid" 2>/dev/null || true
    fi
    cmux_claude_finish_hook_settings >/dev/null 2>&1 || true
    cmux_claude_hook_settings_value=""
}

# Pass through if not in a cmux terminal, hooks are disabled, or the cmux
# socket is unavailable (stale env / app not running).
IN_CMUX=0
if [[ -n "$CMUX_SURFACE_ID" ]]; then
    IN_CMUX=1
fi

if [[ "$IN_CMUX" == "1" ]]; then
    cmux_claude_wrapper_validate_arguments "$@" || exit $?
fi

# CMUX_AGENT_RESTORE_LAUNCH is a provider/session-bound one-shot token attached
# only to app-generated restore input. Consume it before every eventual exec so
# it can authorize this launch without leaking to Claude or hooks.
cmux_claude_app_restore_launch="${CMUX_AGENT_RESTORE_LAUNCH:-}"
unset CMUX_AGENT_RESTORE_LAUNCH

clear_cmux_claude_passthrough_env() {
    if [[ "$IN_CMUX" == "1" ]]; then
        clear_inherited_claude_session_identity_env
        local cmux_key
        for cmux_key in "${!CMUX_@}"; do
            unset "$cmux_key"
        done
        unset TERMINFO
    fi
}

exec_real_claude_passthrough() {
    # Passthrough launches get no hooks; drop any speculative settings job.
    cmux_claude_discard_hook_settings
    clear_cmux_claude_passthrough_env
    cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" "$@"
}

# Find real claude.
REAL_CLAUDE="$(find_real_claude)" || { echo "Error: claude not found in PATH" >&2; exit 127; }

ensure_node_options_restore_module() {
    # The restore preload must outlive the Claude session: every Node child
    # loads it via NODE_OPTIONS, and macOS purges $TMPDIR under long-lived
    # sessions (https://github.com/manaflow-ai/cmux/issues/12022). Keep it
    # next to the other CLI shims in ~/.cmuxterm.
    [[ -n "${HOME:-}" && "$HOME" == /* ]] || return 1
    local guard_dir="${HOME%/}/.cmuxterm/cmux-claude-node-options"
    local guard_path="$guard_dir/restore-node-options.cjs"
    [[ ! -L "$guard_dir" && ! -L "$guard_path" ]] || return 1
    mkdir -p "$guard_dir" || return 1
    chmod 0700 "$guard_dir" || return 1
    local temp_path
    temp_path="$(mktemp "$guard_dir/.restore-node-options.cjs.XXXXXX")" || return 1
    cat >"$temp_path" <<'EOF'
const hadOriginalNodeOptions = process.env.CMUX_ORIGINAL_NODE_OPTIONS_PRESENT === "1";
if (hadOriginalNodeOptions) {
  process.env.NODE_OPTIONS = process.env.CMUX_ORIGINAL_NODE_OPTIONS ?? "";
} else {
  delete process.env.NODE_OPTIONS;
}
delete process.env.CMUX_ORIGINAL_NODE_OPTIONS;
delete process.env.CMUX_ORIGINAL_NODE_OPTIONS_PRESENT;
EOF
    chmod 0644 "$temp_path" || {
        rm -f "$temp_path"
        return 1
    }
    if [[ -f "$guard_path" ]] && cmp -s "$temp_path" "$guard_path"; then
        rm -f "$temp_path" || return 1
    else
        mv -f "$temp_path" "$guard_path" || {
            rm -f "$temp_path"
            return 1
        }
    fi
    printf '%s' "$guard_path"
}

node_options_require_flag() {
    local path="$1"
    # NODE_OPTIONS splits on whitespace; quote the path when $HOME has spaces.
    if [[ "$path" == *[[:space:]]* ]]; then
        printf '%s' "--require=\"$path\""
    else
        printf '%s' "--require=$path"
    fi
}

merge_node_options() {
    local guard_path="$1"
    local require_flag
    require_flag="$(node_options_require_flag "$guard_path")"
    local memory_flag="--max-old-space-size=4096"
    local existing="${NODE_OPTIONS:-}"
    local -a filtered=()
    local -a tokens=()
    local token
    local skip_next=0

    if [[ -z "$guard_path" ]]; then
        printf '%s' "$existing"
        return 0
    fi

    read -r -a tokens <<<"$existing"
    for token in "${tokens[@]}"; do
        if (( skip_next )); then
            skip_next=0
            continue
        fi
        if [[ "$token" == "--max-old-space-size" ]]; then
            skip_next=1
            continue
        fi
        if [[ "$token" == --max-old-space-size=* ]]; then
            continue
        fi
        filtered+=("$token")
    done

    if (( ${#filtered[@]} == 0 )); then
        printf '%s %s' "$require_flag" "$memory_flag"
        return 0
    fi
    printf '%s %s' "$require_flag" "$memory_flag"
    printf ' %s' "${filtered[@]}"
}

normalize_node_options_for_restore() {
    local existing="${1:-}"
    local -a tokens=()
    local -a normalized=()
    local token
    local index=0

    read -r -a tokens <<<"$existing"
    while (( index < ${#tokens[@]} )); do
        token="${tokens[$index]}"
        if [[ "$token" == "--max-old-space-size" && $((index + 1)) -lt ${#tokens[@]} ]]; then
            normalized+=("--max-old-space-size=${tokens[$((index + 1))]}")
            index=$((index + 2))
            continue
        fi
        normalized+=("$token")
        index=$((index + 1))
    done

    if (( ${#normalized[@]} == 0 )); then
        return 0
    fi
    printf '%s' "${normalized[0]}"
    if (( ${#normalized[@]} > 1 )); then
        printf ' %s' "${normalized[@]:1}"
    fi
}

cmux_claude_wrapper_resolve_symlinks() {
    local path="$1" link hops=0
    while [[ -L "$path" && $hops -lt 40 ]]; do
        link="$(readlink "$path")" || break
        case "$link" in
            /*) path="$link" ;;
            *) path="$(dirname "$path")/$link" ;;
        esac
        hops=$((hops + 1))
    done
    printf '%s' "$path"
}

cmux_claude_wrapper_target_is_native_executable() {
    # Volta and mise shims are native launchers (symlinks to their own binary)
    # that may run a Node claude, so they keep the restore preload.
    case "$(basename "$(cmux_claude_wrapper_resolve_symlinks "$1")")" in
        volta-shim|mise|rtx) return 1 ;;
    esac
    local magic
    magic="$(LC_ALL=C od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' \n')" || return 1
    case "$magic" in
        # Mach-O 64/32-bit, universal (fat), and ELF.
        cffaedfe|cefaedfe|feedfacf|feedface|cafebabe|cafebabf|7f454c46) return 0 ;;
    esac
    return 1
}

# Removes the restore preload cmux injects, and the heap flag merge_node_options
# places right after it, keeping every other option.
strip_cmux_node_options() {
    local -a tokens=()
    local -a kept=()
    local index=0
    local token
    read -r -a tokens <<<"$1"
    while (( index < ${#tokens[@]} )); do
        token="${tokens[$index]}"
        index=$((index + 1))
        # node_options_require_flag quotes the path when $HOME has spaces;
        # rejoin the quoted flag before matching it.
        if [[ "$token" == --require=\"* ]]; then
            while [[ "$token" != --require=\"*\" && $index -lt ${#tokens[@]} ]]; do
                token+=" ${tokens[$index]}"
                index=$((index + 1))
            done
        fi
        if [[ "$token" == --require=*/cmux-claude-node-options/restore-node-options.cjs ||
              "$token" == --require=\"*/cmux-claude-node-options/restore-node-options.cjs\" ]]; then
            if [[ "${tokens[$index]:-}" == "--max-old-space-size=4096" ]]; then
                index=$((index + 1))
            fi
            continue
        fi
        kept+=("$token")
    done
    if (( ${#kept[@]} > 0 )); then
        printf '%s' "${kept[*]}"
    fi
}

install_cmux_node_options() {
    # A native claude never runs the --require restore module, so the injected
    # NODE_OPTIONS would leak to every child it spawns, including sandboxed
    # apps that cannot read the module. https://github.com/manaflow-ai/cmux/issues/14681
    # An earlier cmux layer (claude-teams, a re-entering shim) may already have
    # injected it, so do the module's restore here instead.
    if cmux_claude_wrapper_target_is_native_executable "$1"; then
        if [[ "${NODE_OPTIONS:-}" == *"/cmux-claude-node-options/restore-node-options.cjs"* ]]; then
            if [[ "${CMUX_ORIGINAL_NODE_OPTIONS_PRESENT:-}" == "1" ]]; then
                export NODE_OPTIONS="${CMUX_ORIGINAL_NODE_OPTIONS:-}"
            else
                local remaining
                remaining="$(strip_cmux_node_options "$NODE_OPTIONS")"
                if [[ -n "$remaining" ]]; then
                    export NODE_OPTIONS="$remaining"
                else
                    unset NODE_OPTIONS
                fi
            fi
        fi
        unset CMUX_ORIGINAL_NODE_OPTIONS CMUX_ORIGINAL_NODE_OPTIONS_PRESENT
        return 0
    fi
    local guard_path
    if guard_path="$(ensure_node_options_restore_module)"; then
        if [[ ${NODE_OPTIONS+x} && "${NODE_OPTIONS:-}" == *"$(node_options_require_flag "$guard_path")"* ]]; then
            return 0
        fi
        if [[ ${NODE_OPTIONS+x} ]]; then
            export CMUX_ORIGINAL_NODE_OPTIONS_PRESENT=1
            export CMUX_ORIGINAL_NODE_OPTIONS="$(normalize_node_options_for_restore "$NODE_OPTIONS")"
        else
            export CMUX_ORIGINAL_NODE_OPTIONS_PRESENT=0
            unset CMUX_ORIGINAL_NODE_OPTIONS
        fi
        export NODE_OPTIONS="$(merge_node_options "$guard_path")"
    fi
}

encode_launch_argv() {
    {
        printf '%s\0' "$REAL_CLAUDE"
        if (( $# > 0 )); then
            printf '%s\0' "$@"
        fi
    } | base64 | tr -d '\n'
}

cmux_claude_wrapper_create_settings_file() {
    local contents="$1"
    local temp_dir="${TMPDIR:-/tmp}"
    local settings_path
    [[ -d "$temp_dir" ]] || temp_dir="/tmp"
    # mktemp gives each launch a private name; do not sweep matching files here,
    # because another surface may still be handing one to Claude.
    settings_path="$(mktemp "${temp_dir%/}/cmux-claude-settings.XXXXXX")" || return 1
    if ! printf '%s' "$contents" >"$settings_path"; then
        rm -f -- "$settings_path"
        return 1
    fi
    printf '%s' "$settings_path"
}

cmux_claude_wrapper_create_settings_values_file() {
    local temp_dir="${TMPDIR:-/tmp}"
    local settings_path
    [[ -d "$temp_dir" ]] || temp_dir="/tmp"
    settings_path="$(mktemp "${temp_dir%/}/cmux-claude-settings-inputs.XXXXXX")" || return 1
    if ! printf '%s\0' "$@" >"$settings_path"; then
        rm -f -- "$settings_path"
        return 1
    fi
    printf '%s' "$settings_path"
}

# Claude Code persists the path-valued --settings argument when a session is
# moved to the background. A file in $TMPDIR can disappear while that daemon
# is still alive, so the artifact Claude may respawn must live under the user's
# durable cmux directory. The merge inputs remain temporary and are removed
# immediately after launch construction.
cmux_claude_wrapper_durable_settings_dir() {
    [[ -n "${HOME:-}" && "$HOME" == /* ]] || return 1
    local cmux_dir="${HOME%/}/.cmuxterm"
    local settings_dir="$cmux_dir/claude-settings"
    # Do not follow a pre-existing link into a location cmux does not own.
    [[ ! -L "$cmux_dir" && ! -L "$settings_dir" ]] || return 1
    umask 077
    mkdir -p "$settings_dir" || return 1
    [[ -d "$cmux_dir" && -d "$settings_dir" ]] || return 1
    chmod 0700 "$cmux_dir" "$settings_dir" || return 1
    printf '%s' "$settings_dir"
}

cmux_claude_wrapper_sha256_file() {
    local source="$1"
    local digest hasher
    if [[ -x /usr/bin/shasum ]]; then
        digest="$(/usr/bin/shasum -a 256 "$source" 2>/dev/null)" || return 1
    elif hasher="$(command -v sha256sum 2>/dev/null)" && [[ -x "$hasher" ]]; then
        digest="$("$hasher" "$source" 2>/dev/null)" || return 1
    else
        return 1
    fi
    digest="${digest%%[[:space:]]*}"
    [[ "$digest" =~ ^[[:xdigit:]]{64}$ ]] || return 1
    printf '%s' "$digest"
}

cmux_claude_wrapper_promote_settings_file() {
    local source="$1"
    local settings_dir digest target temporary
    [[ -f "$source" ]] || return 1
    settings_dir="$(cmux_claude_wrapper_durable_settings_dir)" || return 1
    digest="$(cmux_claude_wrapper_sha256_file "$source")" || return 1
    target="$settings_dir/${digest:0:32}.json"

    if [[ -f "$target" && ! -L "$target" ]] && cmp -s "$source" "$target"; then
        chmod 0600 "$target" || return 1
        touch "$target" || return 1
        printf '%s' "$target"
        return 0
    fi

    temporary="$(mktemp "$settings_dir/.settings.XXXXXX")" || return 1
    if ! (umask 077; cat "$source" >"$temporary") || ! chmod 0600 "$temporary"; then
        rm -f -- "$temporary"
        return 1
    fi
    if ! mv -f -- "$temporary" "$target"; then
        rm -f -- "$temporary"
        return 1
    fi
    printf '%s' "$target"
}

cmux_claude_wrapper_create_durable_settings_file() {
    local contents="$1"
    local settings_dir temporary durable_path
    settings_dir="$(cmux_claude_wrapper_durable_settings_dir)" || return 1
    temporary="$(mktemp "$settings_dir/.settings.XXXXXX")" || return 1
    if ! (umask 077; printf '%s' "$contents" >"$temporary") || ! chmod 0600 "$temporary"; then
        rm -f -- "$temporary"
        return 1
    fi
    durable_path="$(cmux_claude_wrapper_promote_settings_file "$temporary")"
    rm -f -- "$temporary"
    [[ -n "$durable_path" ]] || return 1
    printf '%s' "$durable_path"
}

# cmux computer use: attach the installed cmux-cua MCP server so any Claude
# session cmux launches can drive the local Mac through cmux's TCC identity.
# Sets CMUX_COMPUTER_USE_CONFIG, or returns nonzero to skip injection so
# sessions never see always-failing tools.
cmux_computer_use_json_escape() {
    local value="$1"
    value="${value//\\/\\\\}"
    value="${value//\"/\\\"}"
    value="${value//$'\n'/\\n}"
    value="${value//$'\r'/\\r}"
    value="${value//$'\t'/\\t}"
    printf '%s' "$value"
}

cmux_claude_load_skill_link_policy() {
    local self_dir resource_dir policy
    self_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)" || return 1
    resource_dir="${self_dir%/bin}"
    policy="$resource_dir/cmux-cua/link-policy.sh"
    [[ -r "$policy" ]] || return 1
    # Keep global-link ownership and collision handling identical to Codex.
    # shellcheck source=/dev/null
    . "$policy"
}

# The client is the cmux Computer Use helper the app installs, selected and
# trust-checked by the policy shared with the Codex wrapper. cmux-cua is a
# closed host-owned contract: never fall back to an ambient executable, which
# would create a second TCC identity. Cleared at startup so the environment
# cannot pre-answer it.
cmux_claude_client_policy_loaded=""
cmux_claude_load_client_policy() {
    [[ "$cmux_claude_client_policy_loaded" == "1" ]] && return 0
    local self_dir resource_dir policy
    self_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)" || return 1
    resource_dir="${self_dir%/bin}"
    policy="$resource_dir/cmux-cua/client-policy.sh"
    [[ -r "$policy" ]] || return 1
    # shellcheck source=/dev/null
    . "$policy" || return 1
    cmux_claude_client_policy_loaded=1
}

# Claude Code managed policy (v2.1.193+) supports disableSideloadFlags, which
# makes claude REFUSE TO START when a non-SDK --mcp-config flag is passed.
# Injecting computer use under that policy would break every cmux Claude
# session on managed machines, so detect it and skip injection.
#
# The flag can arrive through several managed-settings channels (per Claude's
# server-managed-settings docs): the base managed-settings.json, the
# managed-settings.d/*.json fragment dir, the com.anthropic.claudecode MDM
# preferences domain, the cached remote-settings.json, and a policyHelper that
# can override all of them. We cannot faithfully replay Claude's full
# precedence/merge (and must not execute an arbitrary policyHelper on the
# launch hot path), so this is deliberately CONSERVATIVE and FAIL-SAFE: skip
# --mcp-config if ANY source shows the flag true OR a policyHelper is
# configured (its output cannot be ruled out). A false positive only turns off
# computer use; it never blocks a Claude launch. All paths are overridable via
# env so tests need no root-owned fixtures.
cmux_claude_json_flag_true() {
    # Grep a JSON file for `"<key>": true`. Best-effort; a false positive only
    # skips computer use.
    local file="$1" key="$2"
    [[ -r "$file" ]] || return 1
    LC_ALL=C /usr/bin/grep -Eq "\"$key\"[[:space:]]*:[[:space:]]*true" "$file" 2>/dev/null
}

cmux_claude_json_has_key() {
    local file="$1" key="$2"
    [[ -r "$file" ]] || return 1
    LC_ALL=C /usr/bin/grep -Eq "\"$key\"[[:space:]]*:" "$file" 2>/dev/null
}

# The policy is read once per wrapper process: a launch asks both when it
# reconciles skill links and when it builds the MCP config. Reset here so an
# inherited environment variable of the same name cannot pre-answer it.
unset cmux_claude_sideload_flags_disabled_result
cmux_claude_sideload_flags_disabled_result=""

cmux_claude_sideload_flags_disabled() {
    case "$cmux_claude_sideload_flags_disabled_result" in
        yes) return 0 ;;
        no) return 1 ;;
    esac
    if cmux_claude_sideload_flags_disabled_uncached; then
        cmux_claude_sideload_flags_disabled_result=yes
        return 0
    fi
    cmux_claude_sideload_flags_disabled_result=no
    return 1
}

cmux_claude_sideload_flags_disabled_uncached() {
    local base_file managed_dir remote_file plist_domain defaults_bin
    base_file="${CMUX_CLAUDE_MANAGED_SETTINGS_FILE:-/Library/Application Support/ClaudeCode/managed-settings.json}"
    managed_dir="${CMUX_CLAUDE_MANAGED_SETTINGS_DIR:-/Library/Application Support/ClaudeCode/managed-settings.d}"
    remote_file="${CMUX_CLAUDE_REMOTE_SETTINGS_FILE:-$HOME/.claude/remote-settings.json}"
    plist_domain="${CMUX_CLAUDE_MANAGED_DEFAULTS_DOMAIN:-com.anthropic.claudecode}"

    # File + fragment-dir + cached-remote JSON sources: flag true, or a
    # policyHelper configured (can't rule out what it would return).
    local f
    for f in "$base_file" "$remote_file"; do
        cmux_claude_json_flag_true "$f" "disableSideloadFlags" && return 0
        cmux_claude_json_has_key "$f" "policyHelper" && return 0
    done
    if [[ -d "$managed_dir" ]]; then
        for f in "$managed_dir"/*.json; do
            [[ -e "$f" ]] || continue
            cmux_claude_json_flag_true "$f" "disableSideloadFlags" && return 0
            cmux_claude_json_has_key "$f" "policyHelper" && return 0
        done
    fi

    # MDM preferences domain: disableSideloadFlags reading as 1, or any
    # configured policyHelper, disqualifies. Keep the per-key reads here:
    # `defaults read <domain>` is a human-readable plist dump whose escaping
    # varies for strings containing newlines and backslashes, while the
    # per-key command has the exact truth semantics this policy needs.
    defaults_bin=/usr/bin/defaults
    if [[ "${CMUX_CLAUDE_SKIP_DEFAULTS:-0}" != "1" && -x "$defaults_bin" ]]; then
        local flag_value
        flag_value="$("$defaults_bin" read "$plist_domain" disableSideloadFlags 2>/dev/null)"
        [[ "$flag_value" == "1" ]] && return 0
        "$defaults_bin" read "$plist_domain" policyHelper >/dev/null 2>&1 && return 0
    fi

    return 1
}

cmux_computer_use_auth_token() {
    local auth_file metadata owner mode token
    auth_file="${CMUX_CUA_AUTH_TOKEN_FILE:-}"
    if [[ -n "$auth_file" && "$auth_file" == /* && -f "$auth_file" && ! -L "$auth_file" ]]; then
        metadata="$(/usr/bin/stat -f '%u %Lp' "$auth_file" 2>/dev/null || true)"
        read -r owner mode <<< "$metadata"
        if [[ "$owner" == "$(/usr/bin/id -u)" && "$mode" == "600" ]]; then
            IFS= read -r token < "$auth_file" || true
            if [[ -n "$token" ]]; then
                printf '%s' "$token"
                return 0
            fi
        fi
    fi
    token="${CMUX_CUA_SOCKET_AUTH_TOKEN:-}"
    [[ -n "$token" ]] || return 1
    printf '%s' "$token"
}

cmux_claude_computer_use_enabled_for_invocation() {
    [[ "${CMUX_COMPUTER_USE_MCP_DISABLED:-0}" == "1" ]] && return 1
    cmux_claude_sideload_flags_disabled && return 1
    local arg
    for arg in "$@"; do
        case "$arg" in
            --strict-mcp-config|--strict-mcp-config=*)
                # Respect explicit MCP isolation (e.g. cmux's headless
                # auto-naming summarizers pass --strict-mcp-config).
                return 1
                ;;
        esac
    done
    return 0
}

cmux_computer_use_config() {
    cmux_claude_computer_use_enabled_for_invocation "$@" || return 1
    local client state_dir default_session cua_socket runtime_scope runtime_uid auth_token
    # Load the policy in this shell: a command substitution would lose it.
    cmux_claude_load_client_policy || return 1
    client="$(cmux_computer_use_resolve_client)" || return 1
    auth_token="$(cmux_computer_use_auth_token)" || return 1
    runtime_scope="${CMUX_CUA_RUNTIME_SCOPE:-${CMUX_TAG:-default}}"
    runtime_scope="$(printf '%s' "$runtime_scope" | LC_ALL=C /usr/bin/tr -c 'A-Za-z0-9_.-' '-' | /usr/bin/sed -E 's/^[.-]+//; s/[.-]+$//' | /usr/bin/cut -c1-64)"
    [[ -n "$runtime_scope" ]] || runtime_scope="default"
    runtime_uid="$(/usr/bin/id -u)"
    state_dir="${CMUX_CUA_STATE_DIR:-${HOME}/Library/Application Support/cmux/cmux-cua/runtime/${runtime_scope}/state}"
    cua_socket="${CMUX_CUA_SOCKET_PATH:-/tmp/cmux-cua-${runtime_uid}/${runtime_scope}/cmux-cua.sock}"
    # Stable per-terminal session id so the branded agent cursor survives helper
    # restarts in the same surface.
    default_session="cmux-${CMUX_SURFACE_ID:-$$}"
    # The helper owns external onboarding. Mark that contract explicitly so
    # protected calls also wait for cmux's post-verification readiness signal.
    CMUX_COMPUTER_USE_CONFIG="$(printf '{"mcpServers":{"cmux-cua":{"command":"%s","args":["mcp","--socket","%s"],"env":{"CMUX_CUA_MCP_FORCE_PROXY":"1","CMUX_CUA_EXTERNAL_PERMISSION_FLOW":"1","CMUX_CUA_DEFAULT_SESSION":"%s","CMUX_CUA_STATE_OWNER_PID":"%s","CMUX_CUA_TELEMETRY_ENABLED":"false","CMUX_CUA_UPDATE_CHECK":"false","CMUX_CUA_CURSOR_GRADIENT":"#12c7f5,#2d8cff,#6c5cff","CMUX_CUA_CURSOR_BLOOM":"#2d8cff","CMUX_CUA_CURSOR_LABEL":"cmux","CMUX_CUA_STATE_DIR":"%s","NODE_OPTIONS":"","BUN_OPTIONS":""}}}}' \
        "$(cmux_computer_use_json_escape "$client")" \
        "$(cmux_computer_use_json_escape "$cua_socket")" \
        "$(cmux_computer_use_json_escape "$default_session")" \
        "$(cmux_computer_use_json_escape "${CMUX_CLAUDE_PID:-$$}")" \
        "$(cmux_computer_use_json_escape "$state_dir")")" || return 1
    # This function must run in the parent shell: an export inside $(...) is
    # lost before Claude launches its MCP child. Keep the credential in that
    # inherited environment, never in the argv-visible MCP JSON.
    export CMUX_CUA_SOCKET_AUTH_TOKEN="$auth_token"
}

cmux_claude_global_skill_install_enabled() {
    # A durable Claude skill link is an explicit user choice, never a side
    # effect of starting an agent.
    case "${CMUX_COMPUTER_USE_INSTALL_GLOBAL_SKILL:-0}" in
        1|true|TRUE|yes|YES) return 0 ;;
    esac
    return 1
}

cmux_claude_reconcile_skill_links() {
    local self_dir resource_dir source_dir install_requested=0
    self_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)" || return 1
    resource_dir="${self_dir%/bin}"
    source_dir="$resource_dir/cmux-cua"
    [[ -f "$source_dir/SKILL.md" ]] || return 1
    cmux_claude_global_skill_install_enabled && install_requested=1

    if ! cmux_claude_load_skill_link_policy; then
        return 0
    fi

    cmux_cua_skill_reconcile claude "$source_dir" "${PWD:-}" "$install_requested" || return 1
}

claude_interactive_entry_flag() {
    case "$1" in
        --print|--print=*|-p|--resume|--resume=*|-r|--continue|-c|\
        --session-id|--session-id=*|--remote-control|--remote-control=*|\
        --from-pr|--from-pr=*|--worktree|--worktree=*|-w|-w=*)
            return 0
            ;;
    esac
    return 1
}

claude_passthrough_option_flag() {
    case "$1" in
        --help|-h|--version|-v)
            return 0
            ;;
    esac
    return 1
}

claude_builtin_command_name() {
    # Compatibility floor for hidden commands and unavailable help. Advertised
    # commands are also discovered below so new Claude releases need no update.
    case "$1" in
        agents|attach|auth|auto-mode|config|api-key|daemon|doctor|gateway|import|install|\
        kill|logs|mcp|respawn|rm|stop|\
        experimental-next|plugin|plugins|project|rc|remote-control|setup-token|\
        ultrareview|update|upgrade)
            return 0
            ;;
    esac
    return 1
}

claude_discovered_command_name() (
    # Keep discovery optional, bounded, and outside the project. Perl ships on
    # macOS and already supplies process deadlines for the other agent wrappers.
    [[ -x /usr/bin/perl && "${HOME:-}" == /* ]] || return 1
    cmux_claude_wrapper_prepare_custom_path_environment "$REAL_CLAUDE"
    clear_inherited_claude_auth_selection_env
    clear_cmux_claude_passthrough_env
    LC_ALL=C /usr/bin/perl - "$REAL_CLAUDE" "$1" \
        "$HOME/Library/Caches/cmux/claude-commands-v1" 2>/dev/null <<'PERL'
use strict;
use warnings;
use Cwd qw(abs_path);
use Digest::SHA qw(sha256_hex);
use File::Path qw(make_path);
use File::Temp qw(tempfile);
use POSIX qw(setpgid);
use Time::HiRes qw(alarm stat time);

my ($executable, $candidate, $directory) = @ARGV;
my $binary = abs_path($executable) or exit 1;
my @metadata = stat($binary);
exit 1 unless @metadata;
my $identity = sha256_hex(join("\0", $binary, @metadata[0, 1, 7],
    map { sprintf("%.9f", $_) } @metadata[9, 10]));
my $cache = "$directory/" . sha256_hex($binary);
my $word = qr/[a-z][a-z0-9_-]*/;
umask 0077;

# A cache is data, never shell code. Ignore malformed/partial files. Atomic
# replacement lets concurrent launches read complete snapshots without a lock.
if (-f $cache && !-l $cache && -s $cache <= 65536 && open(my $file, "<", $cache)) {
    my @lines = <$file>;
    close $file;
    chomp @lines;
    my ($cached_identity, $expires, @commands) = @lines;
    if (defined($expires) && $cached_identity eq $identity &&
        $expires =~ /^\d+$/ && $expires > time() &&
        !grep { !/^$word$/ } @commands) {
        exit((grep { $_ eq $candidate } @commands) ? 0 : 1);
    }
}

pipe(my $reader, my $writer) or exit 1;
my $pid = fork();
exit 1 unless defined $pid;
if ($pid == 0) {
    # Own the probe process group so a launcher that spawns children cannot
    # keep the pipe open or survive its deadline. Never read the terminal.
    setpgid(0, 0) == 0 or exit 1;
    close $reader;
    open(STDIN, "<", "/dev/null") or exit 1;
    open(STDOUT, ">&", $writer) or exit 1;
    close $writer;
    $ENV{NO_COLOR} = 1;
    exec { $executable } $executable, "--help";
    exit 1;
}
close $writer;
my $stop_probe = sub {
    return unless defined $pid;
    kill "KILL", -$pid;
    kill "KILL", $pid;
    waitpid($pid, 0);
    undef $pid;
};
$SIG{HUP} = sub { $stop_probe->(); exit 129; };
$SIG{INT} = sub { $stop_probe->(); exit 130; };
$SIG{TERM} = sub { $stop_probe->(); exit 143; };
my $output = "";
my $success = eval {
    local $SIG{ALRM} = sub { die "help deadline\n" };
    alarm(0.75);
    while (1) {
        my $read = sysread($reader, my $chunk, 8192);
        die "help read\n" unless defined $read;
        last unless $read;
        $output .= $chunk;
        die "help too large\n" if length($output) > 65536;
    }
    waitpid($pid, 0) == $pid or die "help wait\n";
    my $status = $?;
    undef $pid;
    alarm(0);
    $status == 0;
};
alarm(0);
if (!defined $success) {
    $stop_probe->();
}
close $reader;

my %commands;
if ($success) {
    $output =~ s/\e\[[0-?]*[ -\/]*[\@-~]//g;
    my $in_commands = 0;
    for my $line (split /\r?\n/, $output) {
        if ($line =~ /^Commands:\s*$/) { $in_commands = 1; next; }
        next unless $in_commands;
        last if $line =~ /^\S/;
        # Only command rows, not wrapped descriptions, options, or examples.
        if ($line =~ /^  ($word(?:\|$word)*)(?:\s|$)/) {
            $commands{$_} = 1 for split /\|/, $1;
        }
    }
}

# Retry failed/empty help after a minute. Also refresh successful discovery
# hourly: a version-manager launcher can select a new binary without changing
# its own path/mtime. Native binary replacements still invalidate immediately.
my $expires = int(time()) + (%commands ? 3600 : 60);
eval {
    make_path($directory, { mode => 0700 });
    my ($file, $temporary) = tempfile(".commands-XXXXXX", DIR => $directory);
    my $saved = print $file join("\n", $identity, $expires, sort keys %commands), "\n";
    $saved = close($file) && $saved;
    unlink $temporary unless $saved && rename($temporary, $cache);
};
exit(exists($commands{$candidate}) ? 0 : 1);
PERL
)

claude_command_like_invocation() {
    local candidate="$1"
    [[ -n "$candidate" ]] || return 1
    case "$candidate" in
        *[![:alnum:]_-]*)
            return 1
            ;;
    esac
    claude_builtin_command_name "$candidate" && return 0
    # Empty launches, explicit session flags and multiword prompts never reach
    # discovery. An unknown single word still gets hooks unless help names it.
    [[ "$candidate" =~ ^[a-z][a-z0-9_-]*$ ]] || return 1
    claude_discovered_command_name "$candidate"
}

should_inject_claude_hooks() {
    (( $# == 0 )) && return 0

    local arg command_status
    local skip_next=false
    for arg in "$@"; do
        if [[ "$skip_next" == true ]]; then
            skip_next=false
            continue
        fi
        case "$arg" in
            --)
                return 0
                ;;
            -*)
                if claude_passthrough_option_flag "$arg"; then
                    return 1
                fi
                if claude_interactive_entry_flag "$arg"; then
                    return 0
                fi
                if [[ "$arg" != *=* ]] && claude_option_consumes_value "$arg"; then
                    skip_next=true
                fi
                continue
                ;;
            *)
                claude_command_like_invocation "$arg"
                command_status=$?
                case "$command_status" in
                    0) return 1 ;;
                    # Cancellation is not a failed lookup: do not start a
                    # prompt session after the user interrupts discovery.
                    129|130|143) exit "$command_status" ;;
                esac
                return 0
                ;;
        esac
    done

    return 0
}

# CMUX_CLAUDE_HOOKS_DISABLED is the documented MASTER opt-out: the wrapper
# stays fully inert — no cmux calls, no computer-use attach, no argv changes
# (the disabled-integration regression asserts exactly this).
if [[ "$CMUX_CLAUDE_HOOKS_DISABLED" == "1" ]]; then
    # User-managed hooks need the original cmux terminal environment.
    if [[ "$IN_CMUX" == "1" ]]; then
        clear_inherited_claude_session_identity_env
    fi
    clear_inherited_claude_auth_selection_env
    unset CMUX_CUA_SOCKET_AUTH_TOKEN
    if [[ "$IN_CMUX" == "1" ]]; then
        reconcile_claude_config_dir_for_resume "$(extract_claude_resume_session_id "$@")"
    fi
    cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" "$@"
fi

if [[ "$IN_CMUX" == "0" ]]; then
    clear_inherited_claude_auth_selection_env
    exec_real_claude_passthrough "$@"
fi

# App-generated restore input plus an explicit resume id proves cmux owns this
# launch, so a transient socket miss cannot permanently strip its hooks or
# Computer Use integration. An unmarked resume from a detached shell/tmux keeps
# the bounded ownership probe and preserves native behavior when its inherited
# cmux environment is stale.
cmux_claude_resume_sid="$(extract_claude_resume_session_id "$@")"
cmux_claude_app_restore_owned=0
if [[ "$cmux_claude_resume_sid" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ \
      && "$cmux_claude_app_restore_launch" == "claude:$cmux_claude_resume_sid" ]]; then
    cmux_claude_app_restore_owned=1
fi
# Start the settings generator now so it overlaps the ping. This is only a
# head start: hook injection collects it later and starts it then if it was
# skipped here. Skip it where hooks are clearly not coming: re-entry forwards
# argv unchanged, a missing socket file passes through before any ping, and
# `--version`/`--help` or a known subcommand as the first argument pass through.
unset cmux_claude_hook_cmux_bin
cmux_claude_hook_cmux_bin=""
if (( cmux_claude_wrapper_reexec_hops == 0 )) &&
   ! claude_passthrough_option_flag "${1:-}" &&
   ! claude_builtin_command_name "${1:-}" &&
   [[ "$cmux_claude_app_restore_owned" == "1" ||
      ( -n "${CMUX_SOCKET_PATH:-}" && -S "${CMUX_SOCKET_PATH:-}" ) ]]; then
    cmux_claude_hook_cmux_bin="$(resolve_hook_cmux_bin)"
    cmux_claude_start_hook_settings "$cmux_claude_hook_cmux_bin" || true
fi
if [[ "$cmux_claude_app_restore_owned" != "1" ]] \
      && ! cmux_socket_available; then
    clear_inherited_claude_session_identity_env
    clear_inherited_claude_auth_selection_env
    reconcile_claude_config_dir_for_resume "$cmux_claude_resume_sid"
    exec_real_claude_passthrough "$@"
fi

if (( cmux_claude_wrapper_reexec_hops > 0 )); then
    # A previous wrapper pass already made the cmux launch decision and may have
    # injected --session-id/--settings. While resolving a finite shim chain,
    # refresh per-process hook metadata, then forward argv unchanged so hooks are
    # not merged again on re-entry.
    unset CLAUDECODE
    clear_inherited_claude_auth_selection_env
    export CMUX_CLAUDE_PID=$$
    export CMUX_CLAUDE_HOOK_CMUX_BIN="$(resolve_hook_cmux_bin)"
    if [[ "$cmux_claude_teams_wrapper_launch" != "1" ]]; then
        # Do not carry a pre-file-handoff launch capture from an older wrapper
        # pass; it may contain the oversized inline settings payload we are
        # replacing here.
        unset CMUX_AGENT_LAUNCH_ARGV_B64
        export CMUX_AGENT_LAUNCH_KIND="claude"
        export CMUX_AGENT_LAUNCH_EXECUTABLE="$REAL_CLAUDE"
        export CMUX_AGENT_LAUNCH_CWD="$PWD"
    fi
    install_cmux_node_options "$REAL_CLAUDE"
    cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" "$@"
fi

# Only inject hooks for Claude session entrypoints. Command-like invocations
# pass through so Claude subcommands do not receive session/hook flags.
if ! should_inject_claude_hooks "$@"; then
    clear_inherited_claude_auth_selection_env
    exec_real_claude_passthrough "$@"
fi

# Unset CLAUDECODE to avoid "nested session" detection — cmux terminals are
# independent sessions even when the parent shell was launched from Claude Code.
clear_inherited_claude_session_identity_env
clear_inherited_claude_auth_selection_env

# Check if the user already specified a session/resume flag.
# If so, don't inject our own --session-id (it would conflict).
SKIP_SESSION_ID=false
if claude_explicit_session_flag_present "$@"; then
    SKIP_SESSION_ID=true
fi

# For an explicit `--resume <id>`, point CLAUDE_CONFIG_DIR at the config root that
# actually holds the transcript so an inherited/foreign config dir cannot turn the
# resume into "No conversation found". https://github.com/manaflow-ai/cmux/issues/6194
reconcile_claude_config_dir_for_resume "$cmux_claude_resume_sid"
if [[ -n "$cmux_claude_resume_sid" ]]; then
    export CMUX_AGENT_RESUME_LAUNCH=1
else
    unset CMUX_AGENT_RESUME_LAUNCH
fi

# Export the wrapper's PID. Because we exec claude below, this PID becomes
# the actual claude process PID, which hooks use for stale-session detection.
export CMUX_CLAUDE_PID=$$
# Headless (`-p`/`--print`) runs share the pane's surface with any interactive
# session there, so the agent message hooks skip them.
export CMUX_CLAUDE_HEADLESS=0
cmux_claude_skip_next=false
for cmux_claude_arg in "$@"; do
    if [[ "$cmux_claude_skip_next" == true ]]; then
        cmux_claude_skip_next=false
        continue
    fi
    case "$cmux_claude_arg" in
        --) break ;;
        -p|--print|--print=*) export CMUX_CLAUDE_HEADLESS=1; break ;;
        -*)
            if [[ "$cmux_claude_arg" != *=* ]] && claude_option_consumes_value "$cmux_claude_arg"; then
                cmux_claude_skip_next=true
            fi
            ;;
    esac
done
unset cmux_claude_arg cmux_claude_skip_next
# A parent session's hook spool belongs to that session; this launch starts its own below.
unset CMUX_CLAUDE_HOOK_SPOOL_DIR
if [[ -z "$cmux_claude_hook_cmux_bin" ]]; then
    cmux_claude_hook_cmux_bin="$(resolve_hook_cmux_bin)"
fi
export CMUX_CLAUDE_HOOK_CMUX_BIN="$cmux_claude_hook_cmux_bin"

# Prints the routed launcher's private `--settings` file when the argv carries
# one that still exists.
claude_launch_subrouter_private_settings_path() {
    local argument value expects_value=0
    for argument in "$@"; do
        [[ "$argument" == "--" ]] && break
        value=""
        if (( expects_value )); then
            value="$argument"
            expects_value=0
        else
            case "$argument" in
                --settings) expects_value=1; continue ;;
                --settings=*) value="${argument#--settings=}" ;;
                *) continue ;;
            esac
        fi
        case "$value" in
            */subrouter-claude-settings-*/settings.json)
                if [[ -f "$value" ]]; then
                    printf '%s\n' "$value"
                    return 0
                fi
                ;;
        esac
    done
    return 1
}

bind_subrouter_claude_resume_marker() {
    unset CMUX_AGENT_LAUNCH_SUBROUTER_CLAUDE_RESUME_COMMAND
    case "${SUBROUTER_CLAUDE_RESUME_COMMAND:-}" in
        "sr claude proxy --resume"|"subrouter claude proxy --resume") ;;
        *) return 0 ;;
    esac
    claude_launch_subrouter_private_settings_path "$@" >/dev/null || return 0
    export CMUX_AGENT_LAUNCH_SUBROUTER_CLAUDE_RESUME_COMMAND="$SUBROUTER_CLAUDE_RESUME_COMMAND"
}

# Records the account a routed launch was pinned to, so a restore keeps it.
# The pin is read from the routing headers the launcher wrote into its private
# settings file for this launch (the file is gone once the launcher exits), so
# it holds however the launcher was invoked or what it appended to the argv it
# forwarded. A pooled launch has no pin header and records nothing.
bind_routed_claude_account() {
    unset CMUX_AGENT_LAUNCH_ROUTED_CLAUDE_ACCOUNT
    local settings_path account
    settings_path="$(claude_launch_subrouter_private_settings_path "$@")" || return 0
    # The headers are one JSON string whose lines are joined by an escaped
    # newline, so the id ends at `\n` or at the closing quote. An id with any
    # other character is left unrecorded rather than cut short.
    account="$(LC_ALL=C sed -nE 's/.*X-Subrouter-Account-ID: ([A-Za-z0-9._@+:=-]+)(\\n|").*/\1/p' "$settings_path" 2>/dev/null | head -n 1)"
    [[ -n "$account" && "$account" != -* && ${#account} -le 256 ]] || return 0
    export CMUX_AGENT_LAUNCH_ROUTED_CLAUDE_ACCOUNT="$account"
}

if [[ "$cmux_claude_teams_wrapper_launch" != "1" ]]; then
    export CMUX_AGENT_LAUNCH_KIND="claude"
    export CMUX_AGENT_LAUNCH_EXECUTABLE="$REAL_CLAUDE"
    export CMUX_AGENT_LAUNCH_CWD="$PWD"
    bind_subrouter_claude_resume_marker "$@"
    bind_routed_claude_account "$@"
fi
install_cmux_node_options "$REAL_CLAUDE"

# Build Claude settings JSON. The bundled CLI owns the lifecycle-hook delivery
# contract so Claude and the installed agent integrations cannot drift. A
# missing/older CLI or malformed output falls back atomically to decision hooks
# only. Lifecycle telemetry is omitted in degraded mode so it cannot reintroduce
# a blocking socket round-trip or disable Claude's native notifications.
cmux_claude_plutil_raw() {
    local value="$1"
    local keypath="$2"
    local expected_type="$3"
    printf '%s' "$value" \
        | /usr/bin/plutil -extract "$keypath" raw -expect "$expected_type" -o - -- - 2>/dev/null
}

cmux_claude_plutil_has_command() {
    local value="$1"
    local event="$2"
    local needle="$3"
    local required_matcher="$4"
    local require_sync="$5"
    local group_count group_index hook_count hook_index matcher command async_value

    group_count="$(cmux_claude_plutil_raw "$value" "hooks.$event" array)" || return 1
    [[ "$group_count" =~ ^[0-9]+$ ]] || return 1
    group_index=0
    while (( group_index < group_count )); do
        if [[ -n "$required_matcher" ]]; then
            matcher="$(cmux_claude_plutil_raw "$value" "hooks.$event.$group_index.matcher" string)" \
                || {
                    group_index=$((group_index + 1))
                    continue
                }
            if [[ "$matcher" != "$required_matcher" ]]; then
                group_index=$((group_index + 1))
                continue
            fi
        fi

        hook_count="$(cmux_claude_plutil_raw "$value" "hooks.$event.$group_index.hooks" array)" \
            || {
                group_index=$((group_index + 1))
                continue
            }
        [[ "$hook_count" =~ ^[0-9]+$ ]] || return 1
        hook_index=0
        while (( hook_index < hook_count )); do
            command="$(cmux_claude_plutil_raw "$value" "hooks.$event.$group_index.hooks.$hook_index.command" string)" \
                || {
                    hook_index=$((hook_index + 1))
                    continue
                }
            if [[ "$command" == *"$needle"* ]]; then
                if [[ "$require_sync" != "1" ]]; then
                    return 0
                fi
                async_value="$(
                    cmux_claude_plutil_raw \
                        "$value" \
                        "hooks.$event.$group_index.hooks.$hook_index.async" \
                        bool \
                        || true
                )"
                if [[ "$async_value" != "true" ]]; then
                    return 0
                fi
            fi
            hook_index=$((hook_index + 1))
        done
        group_index=$((group_index + 1))
    done
    return 1
}

cmux_claude_plutil_hook_settings_are_safe() {
    local value="$1"
    local preferred_channel event subcommand

    cmux_claude_plutil_has_command \
        "$value" "PreToolUse" "hooks claude cron-create-guard" "CronCreate" 1 \
        || return 1
    cmux_claude_plutil_has_command \
        "$value" "PermissionRequest" "hooks feed --source claude" "" 1 \
        || return 1

    preferred_channel="$(
        cmux_claude_plutil_raw "$value" preferredNotifChannel string || true
    )"
    if [[ "$preferred_channel" == "notifications_disabled" ]]; then
        while read -r event subcommand; do
            cmux_claude_plutil_has_command \
                "$value" "$event" "hooks enqueue claude $subcommand" "" 0 \
                || return 1
        done <<'CMUX_CLAUDE_LIFECYCLE_HOOKS'
SessionStart session-start
Stop stop
SessionEnd session-end
Notification notification
UserPromptSubmit prompt-submit
CMUX_CLAUDE_LIFECYCLE_HOOKS
    fi
    return 0
}

# Sets cmux_claude_standard_hook_settings_value to the exact bytes the bundled
# CLI prints for `cmux hooks claude inject-settings`
# (CLI/CMUXCLI+ClaudeHookSettings.swift: JSONSerialization with sorted keys
# and unescaped slashes). The text is fixed: it does not depend on the
# environment, the socket, or user settings.
cmux_claude_build_standard_hook_settings() {
    local queued direct_cli
    # JSON-escaped body of AgentHookSpoolProducer(agent: "claude").command(...)
    # wrapping CMUXCLI.queuedAgentHookShellCommand(agent: "claude") as its fallback.
    IFS= read -r queued <<'CMUX_CLAUDE_QUEUED_HOOK_COMMAND'
if [ -n \"${CMUX_CLAUDE_HOOK_SPOOL_DIR:-}\" ] && [ -x /bin/zsh ]; then exec /bin/zsh -fc 'LC_ALL=C\nexport CMUX_CLAUDE_PID=${CMUX_CLAUDE_PID:-$PPID}\nexport CMUX_AGENT_HOOK_INVOKER_PID=$PPID\ncmux_fallback() { { print -rn -- \"$cmux_p\"; /bin/cat; } | /bin/sh -c \"$1\" >&3; exit $?; }\nzmodload zsh/system zsh/datetime zsh/files 2>/dev/null || exec /bin/sh -c \"$1\" >&3\ncmux_p= cmux_c= cmux_e=0\nwhile (( ${#cmux_p} < 262144 )); do sysread -i 0 -s 65536 cmux_c || { cmux_e=$?; break; }; cmux_p+=$cmux_c; done\n[[ -n ${CMUX_SURFACE_ID:-} && ${CMUX_CLAUDE_HOOKS_DISABLED:-} != 1 ]] || { (( cmux_e == 5 )) || /bin/cat >/dev/null; print -r -- '\\''{}'\\'' >&3; exit 0; }\ncmux_d=$CMUX_CLAUDE_HOOK_SPOOL_DIR\n(( cmux_e == 5 )) && [[ -f $cmux_d/keys ]] || cmux_fallback \"$1\"\ncmux_r=\"cmux-agent-hook-v1\"$'\\''\\n'\\''\"claude\"$'\\''\\n'\\''\"$2\"$'\\''\\n'\\''\nfor cmux_k in ${(f)\"$(<$cmux_d/keys)\"}; do (( ${+parameters[$cmux_k]} )) && cmux_r+=\"$cmux_k=${(P)cmux_k}\"$'\\''\\0'\\''; done\ncmux_r+=$'\\''\\0'\\''\"$cmux_p\"\n(( ${#cmux_r} <= 524288 )) || cmux_fallback \"$1\"\numask 077\ncmux_n=$cmux_d/$epochtime[1].$epochtime[2]-$$\n{ print -rn -- \"$cmux_r\" >| $cmux_n.tmp && mv -f -- $cmux_n.tmp $cmux_n.rec; } 2>/dev/null || { rm -f -- $cmux_n.tmp 2>/dev/null; cmux_fallback \"$1\"; }\nif zsystem flock -t 0 -r -f cmux_l $cmux_d/forwarder.lock 2>/dev/null; then\n  zsystem flock -u $cmux_l\n  rm -- $cmux_n.rec 2>/dev/null && cmux_fallback \"$1\"\nfi\nprint -r -- '\\''{}'\\'' >&3' cmux-hook 'cmux_cli=\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-${CMUX_BUNDLED_CLI_PATH:-}}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; agent_pid=\"${CMUX_CLAUDE_PID:-${PPID:-}}\"; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$CMUX_CLAUDE_HOOKS_DISABLED\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then CMUX_CLAUDE_PID=\"$agent_pid\" CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.5 \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" hooks enqueue claude @SUBCOMMAND@ 2>/dev/null || { cat >/dev/null; echo '\\''{}'\\''; }; else CMUX_CLAUDE_PID=\"$agent_pid\" CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.5 \"$cmux_cli\" hooks enqueue claude @SUBCOMMAND@ 2>/dev/null || { cat >/dev/null; echo '\\''{}'\\''; }; fi; else cat >/dev/null; echo '\\''{}'\\''; fi' @SUBCOMMAND@ 3>&1 1>&2; else cmux_cli=\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-${CMUX_BUNDLED_CLI_PATH:-}}\"; if [ -z \"$cmux_cli\" ] || [ ! -x \"$cmux_cli\" ]; then cmux_cli=\"$(command -v cmux 2>/dev/null || true)\"; fi; agent_pid=\"${CMUX_CLAUDE_PID:-${PPID:-}}\"; if [ -n \"$CMUX_SURFACE_ID\" ] && [ \"$CMUX_CLAUDE_HOOKS_DISABLED\" != \"1\" ] && [ -n \"$cmux_cli\" ]; then if [ -n \"${CMUX_SOCKET_PATH:-}\" ]; then CMUX_CLAUDE_PID=\"$agent_pid\" CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.5 \"$cmux_cli\" --socket \"$CMUX_SOCKET_PATH\" hooks enqueue claude @SUBCOMMAND@ 2>/dev/null || { cat >/dev/null; echo '{}'; }; else CMUX_CLAUDE_PID=\"$agent_pid\" CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC=0.5 \"$cmux_cli\" hooks enqueue claude @SUBCOMMAND@ 2>/dev/null || { cat >/dev/null; echo '{}'; }; fi; else cat >/dev/null; echo '{}'; fi; fi
CMUX_CLAUDE_QUEUED_HOOK_COMMAND
    direct_cli='\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\"'

    local q_prefix='{"hooks":[{"command":"' q_suffix='","timeout":5,"type":"command"}],"matcher":"'
    local notification="${queued//@SUBCOMMAND@/notification}"
    local push_notification="${queued//@SUBCOMMAND@/push-notification}"
    local pre_tool_use="${queued//@SUBCOMMAND@/pre-tool-use}"
    local session_end="${queued//@SUBCOMMAND@/session-end}"
    local session_start="${queued//@SUBCOMMAND@/session-start}"
    local stop="${queued//@SUBCOMMAND@/stop}"
    local feed="${queued//@SUBCOMMAND@/feed}"
    local prompt_submit="${queued//@SUBCOMMAND@/prompt-submit}"
    # Background agent-message wait hook (asyncRewake), on SessionStart and Stop.
    local inbox_wait='{"hooks":[{"async":true,"asyncRewake":true,"command":"'"$direct_cli"' hooks claude inbox-wait","timeout":86400,"type":"command"}],"matcher":""}'

    cmux_claude_standard_hook_settings_value='{"hooks":{'
    cmux_claude_standard_hook_settings_value+='"Notification":['"$q_prefix$notification$q_suffix"'"}],'
    cmux_claude_standard_hook_settings_value+='"PermissionRequest":[{"hooks":[{"command":"'"$direct_cli"' hooks feed --source claude","timeout":125,"type":"command"}],"matcher":""}],'
    cmux_claude_standard_hook_settings_value+='"PostToolUse":['"$q_prefix$push_notification$q_suffix"'PushNotification"}],'
    cmux_claude_standard_hook_settings_value+='"PreToolUse":[{"hooks":[{"command":"'"$direct_cli"' hooks claude cron-create-guard","timeout":5,"type":"command"}],"matcher":"CronCreate"},'"$q_prefix$pre_tool_use$q_suffix"'"}],'
    cmux_claude_standard_hook_settings_value+='"SessionEnd":['"$q_prefix$session_end$q_suffix"'"}],'
    cmux_claude_standard_hook_settings_value+='"SessionStart":['"$q_prefix$session_start$q_suffix"'"},'"$inbox_wait"'],'
    cmux_claude_standard_hook_settings_value+='"Stop":['"$q_prefix$stop$q_suffix"'"},'"$q_prefix$feed$q_suffix"'"},{"hooks":[{"async":true,"command":"'"$direct_cli"' hooks claude auto-name","timeout":120,"type":"command"}],"matcher":""},'"$inbox_wait"'],'
    cmux_claude_standard_hook_settings_value+='"StopFailure":['"$q_prefix$stop$q_suffix"'"},'"$inbox_wait"'],'
    cmux_claude_standard_hook_settings_value+='"SubagentStop":['"$q_prefix$feed$q_suffix"'"}],'
    cmux_claude_standard_hook_settings_value+='"UserPromptSubmit":['"$q_prefix$prompt_submit$q_suffix"'"},{"hooks":[{"command":"'"$direct_cli"' hooks claude inbox-drain 2>/dev/null || echo '"'"'{}'"'"'","timeout":5,"type":"command"}],"matcher":""}]'
    cmux_claude_standard_hook_settings_value+='},"preferredNotifChannel":"notifications_disabled"}'
}

# Fast path for the Node check below. It accepts one exact string: the
# bundled CLI's standard document, which satisfies every Node requirement
# (valid JSON object; synchronous cron-create-guard under the CronCreate
# matcher; synchronous PermissionRequest feed; all five lifecycle hooks since
# it sets notifications_disabled). Any other byte sequence, including a
# different CLI version's output, returns 1 and takes the Node path unchanged.
cmux_claude_generated_hook_settings_are_standard() {
    local value="$1"
    local cmux_claude_standard_hook_settings_value=""
    cmux_claude_build_standard_hook_settings
    [[ -n "$cmux_claude_standard_hook_settings_value" &&
          "$value" == "$cmux_claude_standard_hook_settings_value" ]]
}

cmux_claude_generated_hook_settings_are_safe() {
    local value="$1"
    (( ${#value} > 0 && ${#value} <= 1048576 )) || return 1
    [[ "$value" =~ ^[[:space:]]*\{ ]] || return 1

    cmux_claude_generated_hook_settings_are_standard "$value" && return 0

    if command -v node >/dev/null 2>&1; then
        printf '%s' "$value" | node -e '
const fs=require("fs");
try {
  const x=JSON.parse(fs.readFileSync(0,"utf8"));
  if(!x||Array.isArray(x)||typeof x!=="object")process.exit(1);
  const hooks=x.hooks;
  if(!hooks||Array.isArray(hooks)||typeof hooks!=="object")process.exit(1);
  const commands=(event,matcher)=>{
    const groups=hooks[event];
    if(!Array.isArray(groups))return [];
    return groups.flatMap(group=>{
      if(!group||typeof group!=="object")return [];
      if(matcher!==undefined&&group.matcher!==matcher)return [];
      return Array.isArray(group.hooks)?group.hooks:[];
    });
  };
  const hasSyncCommand=(event,needle,matcher)=>commands(event,matcher).some(hook=>
    hook&&typeof hook==="object"&&typeof hook.command==="string"&&
    hook.command.includes(needle)&&hook.async!==true
  );
  if(!hasSyncCommand("PreToolUse","hooks claude cron-create-guard","CronCreate"))process.exit(1);
  if(!hasSyncCommand("PermissionRequest","hooks feed --source claude"))process.exit(1);
  if(x.preferredNotifChannel==="notifications_disabled"){
    const lifecycle=[
      ["SessionStart","session-start"],
      ["Stop","stop"],
      ["SessionEnd","session-end"],
      ["Notification","notification"],
      ["UserPromptSubmit","prompt-submit"],
    ];
    if(!lifecycle.every(([event,subcommand])=>
      commands(event).some(hook=>
        hook&&typeof hook==="object"&&typeof hook.command==="string"&&
        hook.command.includes(`hooks enqueue claude ${subcommand}`)
      )
    ))process.exit(1);
  }
}
catch (_) { process.exit(1); }
' >/dev/null 2>&1
        return $?
    fi
    if [[ -x /usr/bin/plutil ]]; then
        cmux_claude_plutil_hook_settings_are_safe "$value"
        return $?
    fi
    return 1
}

# Claude Code merges --settings additively with the user's own settings.json.
# Generated settings also set preferredNotifChannel to disable Claude Code's OSC/terminal notification
#   channel inside cmux so hooks are the only notification source.
# - SessionStart/Stop/Notification: existing lifecycle hooks
# - StopFailure: a turn that died on an API error (usage limit, dropped
#   connection) shows the error instead of staying "Running"
# - SessionEnd: cleanup when Claude exits (covers Ctrl+C where Stop doesn't fire)
# - UserPromptSubmit: clears "Needs input" and sets "Running" on new prompt
# - PreToolUse: rejects unsupported durable cron requests synchronously, then
#   (async) clears "Needs input" / sets "Running" for ordinary tools, and flags
#   "Needs input" for the blocking AskUserQuestion / ExitPlanMode tools. With
#   --dangerously-skip-permissions (permission_mode "bypassPermissions") Claude
#   fires no PermissionRequest/Notification for those two, so the handler also
#   publishes the full needs-input status + bell itself. See
#   https://github.com/manaflow-ai/cmux/issues/6606
# - PostToolUse (PushNotification): bridges Claude's PushNotification tool
#   into a cmux notification. The tool delivers via a raw OSC desktop
#   notification, which cmux suppresses for surfaces running a hook-integrated
#   agent, and it never fires the Notification hook — so this PostToolUse
#   bridge is the only way its message reaches the user inside cmux. ASYNC;
#   the handler mirrors the tool's own sent/skipped decision via
#   tool_response.localSent.
# - PermissionRequest: cmux hooks feed. SYNC with a 125s timeout.
#   This is Claude Code's native blocking decision hook. Outside
#   bypassPermissions it covers permissions, ExitPlanMode, and AskUserQuestion
#   without using PreToolUse denial as a side channel; under bypassPermissions it
#   does not fire, and PreToolUse is the needs-input fallback (see above).
# - SubagentStop: feed telemetry only. It must not run the visible Stop
#   hook because a subagent finishing should not notify like the parent.
# - Stop auto-name: opt-in AI workspace/tab naming. ASYNC with a long
#   timeout because it may run a summarization subprocess; it gates itself
#   on the workspaceAutoNaming setting via a socket probe, so it is a
#   no-op when the feature is disabled.
HOOKS_JSON='{"hooks":{"PreToolUse":[{"matcher":"CronCreate","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks claude cron-create-guard","timeout":5}]}],"PermissionRequest":[{"matcher":"","hooks":[{"type":"command","command":"\"${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}\" hooks feed --source claude","timeout":125}]}]}}'
if cmux_claude_collect_hook_settings "$CMUX_CLAUDE_HOOK_CMUX_BIN" \
    && cmux_claude_generated_hook_settings_are_safe "$cmux_claude_hook_settings_value"; then
    HOOKS_JSON="$cmux_claude_hook_settings_value"
fi
unset cmux_claude_hook_settings_value

# Fold any user-provided --settings into HOOKS_JSON before launching.
#
# cmux injects its hook configuration via --settings. Passing it as a separate
# flag alongside a user's own --settings is fragile: with two --settings on the
# command line Claude Code reads only ONE of them (a pure replace, not a merge),
# and which one is undocumented and has already changed across CLI versions --
# first-wins on <=2.1.168 (the user's flag was dropped: the original #2816
# symptom), last-wins on >=2.1.169 (the user's flag wins but cmux's hook
# --settings is dropped, silently breaking cmux notifications/status). Either
# way one side is lost. Deep-merge the user's settings into ours, write the
# combined document to a private settings file, and pass one path-valued
# --settings flag. Hook arrays from a genuine user document are retained, while
# cmux's marked block is replaced on re-entry. This is precedence-agnostic -- it
# does not rely on the CLI's multi-flag behavior. Requires `node`; if node is
# missing or the merge fails, preserve the user's original settings and disable
# cmux hooks for that launch rather than passing an ambiguous pair of flags.
unset CMUX_USER_SETTINGS_B64
CMUX_FILTERED_ARGS=()
CMUX_USER_SETTINGS=()
cmux_collect_settings_value=false
cmux_pending_option_value=false
cmux_positional_only=false
for arg in "$@"; do
    if [[ "$cmux_positional_only" == true ]]; then
        CMUX_FILTERED_ARGS+=("$arg")
        continue
    fi
    if [[ "$cmux_collect_settings_value" == true ]]; then
        CMUX_USER_SETTINGS+=("$arg")
        cmux_collect_settings_value=false
        continue
    fi
    if [[ "$cmux_pending_option_value" == true ]]; then
        CMUX_FILTERED_ARGS+=("$arg")
        cmux_pending_option_value=false
        continue
    fi
    if [[ "$arg" == "--" ]]; then
        cmux_positional_only=true
        CMUX_FILTERED_ARGS+=("$arg")
    elif [[ "$arg" == "--settings" ]]; then
        # The fixed-width prefix checks above avoid a wildcard match against a
        # potentially very large JSON value.
        cmux_collect_settings_value=true
    elif [[ "${arg:0:11}" == "--settings=" ]]; then
        CMUX_USER_SETTINGS+=("${arg#--settings=}")
    elif [[ "${arg:0:1}" == "-" ]]; then
        CMUX_FILTERED_ARGS+=("$arg")
        if [[ "${arg%%=*}" == "$arg" ]] && claude_option_consumes_value "$arg"; then
            cmux_pending_option_value=true
        fi
    else
        CMUX_FILTERED_ARGS+=("$arg")
    fi
done
if [[ "$cmux_collect_settings_value" == true ]]; then
    # Dangling --settings with no value; keep it so the real CLI reports the error.
    CMUX_FILTERED_ARGS+=("--settings")
fi

CMUX_SETTINGS_PATH=""
CMUX_SETTINGS_BASE_PATH=""
CMUX_SETTINGS_INPUTS_PATH=""
CMUX_MERGED_SETTINGS_PATH=""
CMUX_SETTINGS_MERGE_SUCCEEDED=false

# The settings artifact is file-backed so re-entry carries a bounded identity
# instead of copying an ever-growing JSON value through argv and the environment.
if (( ${#CMUX_USER_SETTINGS[@]} > 0 )); then
    if CMUX_SETTINGS_BASE_PATH="$(cmux_claude_wrapper_create_settings_file "$HOOKS_JSON")"; then
        CMUX_SETTINGS_PATH="$CMUX_SETTINGS_BASE_PATH"
    else
        printf 'cmux: warning: unable to create hook settings file; hooks were not injected\n' >&2
    fi
elif CMUX_SETTINGS_BASE_PATH="$(cmux_claude_wrapper_create_durable_settings_file "$HOOKS_JSON")"; then
    # There is no merge input, so this is already the final persisted artifact.
    CMUX_SETTINGS_PATH="$CMUX_SETTINGS_BASE_PATH"
else
    printf 'cmux: warning: unable to create hook settings file; hooks were not injected\n' >&2
fi

if (( ${#CMUX_USER_SETTINGS[@]} > 0 )) && [[ -n "$CMUX_SETTINGS_BASE_PATH" ]] && command -v node >/dev/null 2>&1; then
    if CMUX_SETTINGS_INPUTS_PATH="$(cmux_claude_wrapper_create_settings_values_file "${CMUX_USER_SETTINGS[@]}")" &&
       CMUX_MERGED_SETTINGS_PATH="$(cmux_claude_wrapper_create_settings_file "")"; then
        CMUX_BASE_SETTINGS_PATH="$CMUX_SETTINGS_BASE_PATH" \
        CMUX_USER_SETTINGS_PATH="$CMUX_SETTINGS_INPUTS_PATH" \
        CMUX_MERGED_SETTINGS_PATH="$CMUX_MERGED_SETTINGS_PATH" \
        node -e '
const fs=require("fs"),os=require("os"),path=require("path");
const isObj=x=>x&&typeof x==="object"&&!Array.isArray(x);
const clone=x=>JSON.parse(JSON.stringify(x));
const canonical=x=>JSON.stringify(x,(key,value)=>{
  if(!isObj(value)) return value;
  return Object.keys(value).sort().reduce((out,k)=>{out[k]=value[k];return out;},{});
});
const marker=x=>isObj(x)&&isObj(x.__cmux)&&x.__cmux.managed==="claude-hooks";
const expand=p=>p.indexOf("~/")===0?path.join(os.homedir(),p.slice(2)):p;
const load=value=>{
  const text=String(value).trim();
  if(text==="") throw new Error("empty --settings value");
  return (text[0]==="{"||text[0]==="[")?JSON.parse(text):JSON.parse(fs.readFileSync(expand(text),"utf8"));
};
const merge=(a,b)=>{
  if(Array.isArray(a)&&Array.isArray(b)) return a.concat(b);
  if(isObj(a)&&isObj(b)){
    const out=Object.assign({},a);
    for(const key of Object.keys(b)) out[key]=(key in a)?merge(a[key],b[key]):clone(b[key]);
    return out;
  }
  if(isObj(a)||Array.isArray(a)) return a;
  return b;
};
const stripManaged=(settings,base,hasMarker)=>{
  if(!isObj(settings)||!isObj(settings.hooks)) return settings;
  const out=clone(settings);
  const fingerprints=new Set(
    hasMarker&&isObj(settings.__cmux)&&Array.isArray(settings.__cmux.hookFingerprints)
      ? settings.__cmux.hookFingerprints
      : []
  );
  for(const event of Object.keys(out.hooks)){
    const values=out.hooks[event];
    if(!Array.isArray(values)) continue;
    const baseValues=isObj(base.hooks)&&Array.isArray(base.hooks[event])?base.hooks[event]:[];
    out.hooks[event]=values.filter(value=>{
      if(baseValues.some(baseValue=>canonical(baseValue)===canonical(value))) return false;
      return !fingerprints.has(canonical(value));
    });
  }
  return out;
};
const base=load(process.env.CMUX_BASE_SETTINGS_PATH);
const raw=fs.readFileSync(process.env.CMUX_USER_SETTINGS_PATH,"utf8");
const values=raw.split("\0").slice(0,-1);
let acc=clone(base);
try{
  for(const value of values.reverse()){
    const loaded=load(value);
    const hasBaseHook=()=>isObj(loaded)&&isObj(loaded.hooks)&&Object.keys(loaded.hooks).some(event=>{
      const values=loaded.hooks[event];
      const baseValues=isObj(base.hooks)&&Array.isArray(base.hooks[event])?base.hooks[event]:[];
      return Array.isArray(values)&&values.some(value=>baseValues.some(baseValue=>canonical(baseValue)===canonical(value)));
    });
    const managed=marker(loaded)||hasBaseHook();
    acc=merge(acc,managed?stripManaged(loaded,base,marker(loaded)):loaded);
  }
}catch(error){
  process.stderr.write("cmux: settings merge failed; using user settings unchanged\n");
  process.exit(1);
}
const baseMarker=clone(base.__cmux||{managed:"claude-hooks",version:1});
const hookFingerprints=[];
for(const event of Object.keys(base.hooks||{})){
  const groups=Array.isArray(base.hooks[event])?base.hooks[event]:[];
  for(const group of groups) hookFingerprints.push(canonical(group));
}
baseMarker.hookFingerprints=hookFingerprints;
acc.__cmux=baseMarker;
fs.writeFileSync(process.env.CMUX_MERGED_SETTINGS_PATH,JSON.stringify(acc),{encoding:"utf8",mode:0o600});
'
        cmux_merge_status=$?
        if [[ $cmux_merge_status -eq 0 && -s "$CMUX_MERGED_SETTINGS_PATH" ]]; then
            if durable_settings_path="$(cmux_claude_wrapper_promote_settings_file "$CMUX_MERGED_SETTINGS_PATH")"; then
                CMUX_SETTINGS_PATH="$durable_settings_path"
                CMUX_SETTINGS_MERGE_SUCCEEDED=true
                set -- "${CMUX_FILTERED_ARGS[@]}"
            fi
            unset durable_settings_path
            if [[ "$CMUX_SETTINGS_MERGE_SUCCEEDED" != true ]]; then
                rm -f -- "$CMUX_MERGED_SETTINGS_PATH"
            fi
        else
            rm -f -- "$CMUX_MERGED_SETTINGS_PATH"
        fi
    fi
fi

if [[ "$CMUX_SETTINGS_MERGE_SUCCEEDED" != true && ${#CMUX_USER_SETTINGS[@]} -gt 0 ]]; then
    printf 'cmux: warning: settings merge failed; cmux hooks are disabled for this launch and your settings were preserved unchanged; install Node or correct the settings file before retrying\n' >&2
    CMUX_SETTINGS_PATH=""
fi
if [[ -n "$CMUX_MERGED_SETTINGS_PATH" ]]; then
    rm -f -- "$CMUX_MERGED_SETTINGS_PATH"
fi
if [[ -n "$CMUX_SETTINGS_INPUTS_PATH" ]]; then
    rm -f -- "$CMUX_SETTINGS_INPUTS_PATH"
fi
if [[ -n "$CMUX_SETTINGS_BASE_PATH" && "$CMUX_SETTINGS_PATH" != "$CMUX_SETTINGS_BASE_PATH" ]]; then
    rm -f -- "$CMUX_SETTINGS_BASE_PATH"
fi
unset CMUX_MERGED_SETTINGS_PATH cmux_merge_status CMUX_SETTINGS_INPUTS_PATH

# Capture only the compact post-merge argv. The previous implementation encoded
# the inline JSON here as a second ARG_MAX multiplier.
if [[ "$cmux_claude_teams_wrapper_launch" != "1" ]]; then
    if [[ "$CMUX_SETTINGS_MERGE_SUCCEEDED" == true ]]; then
        # A successful merge has replaced settings with the private file path,
        # so the filtered argv is the authoritative compact launch snapshot.
        export CMUX_AGENT_LAUNCH_ARGV_B64="$(encode_launch_argv "${CMUX_FILTERED_ARGS[@]}")"
    else
        # Preserve the caller's original settings in restore metadata when the
        # merge is unavailable; the argument-size guard keeps this bounded.
        export CMUX_AGENT_LAUNCH_ARGV_B64="$(encode_launch_argv "$@")"
    fi
fi

# Reconcile the bundled skill's discovery path before the session starts.
# Normal launches clean up only links proven to be cmux-managed. Skill
# discovery remains inert unless durable installation is explicitly requested.
# CMUX_COMPUTER_USE_INSTALL_GLOBAL_SKILL=1 is the explicit opt-in for a durable
# user-global link.
if cmux_claude_computer_use_enabled_for_invocation "$@"; then
    cmux_claude_reconcile_skill_links || true
fi

# Attach the cmux computer use MCP server. Prepended so a user-supplied
# --mcp-config still applies as an additional config (the CLI merges repeated
# --mcp-config flags). Must be the single-token `--mcp-config=<json>` form:
# --mcp-config is variadic (`<configs...>`), so a separate value token would
# make a following positional prompt (`claude "fix this"`) parse as another
# MCP config. Injected after launch-argv capture on purpose: resume re-runs
# the wrapper, which re-injects against the machinery present then.
CMUX_COMPUTER_USE_CONFIG=""
if ! cmux_computer_use_config "$@"; then
    CMUX_COMPUTER_USE_CONFIG=""
    # CMUX_CUA_SOCKET_AUTH_TOKEN is a private cmux launch credential. Do not
    # carry an inherited or partially-resolved value into Claude when the MCP
    # attachment was rejected or could not be built.
    unset CMUX_CUA_SOCKET_AUTH_TOKEN
fi
if [[ -n "$CMUX_COMPUTER_USE_CONFIG" ]]; then
    set -- "--mcp-config=$CMUX_COMPUTER_USE_CONFIG" "$@"
fi

# One private hook spool and forwarder per session. Queued hooks then publish
# events with zsh builtins instead of launching the cmux CLI for every event;
# the forwarder admits them over one socket connection. The exec below keeps
# this PID, so the forwarder is Claude's child and exits after Claude does.
# Remote relays and settings without the spool path keep the CLI transport.
if [[ -n "$CMUX_SETTINGS_PATH" && "$OSTYPE" == darwin* && -x /bin/zsh
      && "$CMUX_CLAUDE_HOOKS_DISABLED" != "1"
      && -x "$CMUX_CLAUDE_HOOK_CMUX_BIN" && "$CMUX_CLAUDE_HOOK_CMUX_BIN" == /*
      && "${CMUX_SOCKET_PATH:-/}" == /* && -z "${CMUX_RELAY_ID:-}"
      && "$HOOKS_JSON" == *CMUX_CLAUDE_HOOK_SPOOL_DIR* ]]; then
    cmux_claude_hook_spool_dir="$(umask 077; mktemp -d "${TMPDIR:-/tmp}/cmux-claude-hooks.XXXXXXXX" 2>/dev/null)" \
        || cmux_claude_hook_spool_dir=""
    if [[ -n "$cmux_claude_hook_spool_dir" ]]; then
        export CMUX_CLAUDE_HOOK_SPOOL_DIR="$cmux_claude_hook_spool_dir"
        # Signals for Claude's process group must not end the forwarder before
        # its final drain; it exits on its own when Claude exits.
        ( trap '' HUP INT QUIT TERM; exec "$CMUX_CLAUDE_HOOK_CMUX_BIN" hooks claude spool-forwarder ) \
            </dev/null >/dev/null 2>&1 &
    fi
    unset cmux_claude_hook_spool_dir
fi

if [[ "$SKIP_SESSION_ID" == true ]]; then
    if [[ -n "$CMUX_SETTINGS_PATH" ]]; then
        cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" --settings "$CMUX_SETTINGS_PATH" "$@"
    else
        cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" "$@"
    fi
else
    SESSION_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
    if [[ -n "$CMUX_SETTINGS_PATH" ]]; then
        cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" --session-id "$SESSION_ID" --settings "$CMUX_SETTINGS_PATH" "$@"
    else
        cmux_claude_wrapper_exec_resolved_claude "$REAL_CLAUDE" --session-id "$SESSION_ID" "$@"
    fi
fi
