cmux/Resources/bin/claude
Achieve b24a53dc06
Add -r shorthand to SKIP_SESSION_ID check in claude wrapper (#1992)
* Add -r shorthand to SKIP_SESSION_ID check in claude wrapper

The wrapper checked for --resume but not its -r shorthand, causing
claude -r to fail with a --session-id conflict error because the
wrapper injected its own --session-id alongside the implicit --resume.

Fixes #1987

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove invalid -r=* pattern from SKIP_SESSION_ID check

Short options don't use the = form, so -r=* would never match a real
CLI invocation. Keep only -r as the shorthand for --resume.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:26:50 -07:00

96 lines
4.2 KiB
Bash
Executable file

#!/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.
# Find the real claude binary, skipping our own directory.
find_real_claude() {
local self_dir
self_dir="$(cd "$(dirname "$0")" && pwd)"
local IFS=:
for d in $PATH; do
[[ "$d" == "$self_dir" ]] && continue
[[ -x "$d/claude" ]] && printf '%s' "$d/claude" && 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
}
# 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" == "0" || "$CMUX_CLAUDE_HOOKS_DISABLED" == "1" ]] || ! cmux_socket_available; then
# In cmux-launched shells, preserve old behavior and always clear nested
# Claude session markers, even when we must pass through due to stale socket.
if [[ "$IN_CMUX" == "1" ]]; then
unset CLAUDECODE
fi
REAL_CLAUDE="$(find_real_claude)" || { echo "Error: claude not found in PATH" >&2; exit 127; }
exec "$REAL_CLAUDE" "$@"
fi
# Find real claude.
REAL_CLAUDE="$(find_real_claude)" || { echo "Error: claude not found in PATH" >&2; exit 127; }
# Pass through subcommands that don't support session/hook flags.
case "${1:-}" in
mcp|config|api-key|rc|remote-control) exec "$REAL_CLAUDE" "$@" ;;
esac
# Unset CLAUDECODE to avoid "nested session" detection — cmux terminals are
# independent sessions even when the parent shell was launched from Claude Code.
unset CLAUDECODE
# 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
for arg in "$@"; do
case "$arg" in
--resume|--resume=*|-r|--session-id|--session-id=*|--continue|-c)
SKIP_SESSION_ID=true
break
;;
esac
done
# 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=$$
# Build hooks settings JSON.
# Claude Code merges --settings additively with the user's own settings.json.
# - SessionStart/Stop/Notification: existing lifecycle hooks
# - SessionEnd: cleanup when Claude exits (covers Ctrl+C where Stop doesn't fire)
# - UserPromptSubmit: clears "Needs input" and sets "Running" on new prompt
# - PreToolUse: clears "Needs input" when Claude resumes after permission grant (async to avoid latency)
HOOKS_JSON='{"hooks":{"SessionStart":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-start","timeout":10}]}],"Stop":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook stop","timeout":10}]}],"SessionEnd":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook session-end","timeout":1}]}],"Notification":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook notification","timeout":10}]}],"UserPromptSubmit":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook prompt-submit","timeout":10}]}],"PreToolUse":[{"matcher":"","hooks":[{"type":"command","command":"cmux claude-hook pre-tool-use","timeout":5,"async":true}]}]}}'
if [[ "$SKIP_SESSION_ID" == true ]]; then
exec "$REAL_CLAUDE" --settings "$HOOKS_JSON" "$@"
else
SESSION_ID="$(uuidgen | tr '[:upper:]' '[:lower:]')"
exec "$REAL_CLAUDE" --session-id "$SESSION_ID" --settings "$HOOKS_JSON" "$@"
fi