|
#!/bin/bash |
|
# Patch Claude Code (2.1.113+) Bun-compiled Mach-O binary so thinking blocks |
|
# auto-expand inline in the live view, without forcing global verbose mode. |
|
# |
|
# Context: https://github.com/anthropics/claude-code/issues/49268 |
|
# |
|
# Up to ~2.1.110 Claude Code shipped a pure-JS `cli.js` that could be patched |
|
# with sed to auto-expand thinking blocks. Starting with 2.1.111–2.1.113 the |
|
# package ships a Bun-compiled native Mach-O binary at |
|
# `node_modules/@anthropic-ai/claude-code/bin/claude.exe`. The minified JS is |
|
# still embedded as readable bytes, but Mach-O load commands reference byte |
|
# offsets and the binary is Developer ID-signed with hardened runtime — so |
|
# every substitution MUST be length-preserving, and the binary must be |
|
# re-signed (ad-hoc) after modification or the kernel SIGKILLs it on exec. |
|
# |
|
# Two length-preserving edits against the embedded JS: |
|
# 1. Kills the thinking-block early return in the live-view render. |
|
# `case"thinking":{if(!X&&!Y...)return null;` |
|
# → `case"thinking":{if(!1&&!Y...)return null;` (!1 is false, short-circuits) |
|
# `!X` (1 + len(X) bytes) → `!1...1` same width, always-false. |
|
# 2. Forces the thinking component alone into expanded render by flipping |
|
# isTranscriptMode AND verbose to truthy values in that specific |
|
# createElement call. Other components keep the real verbose state, so |
|
# tool output stays collapsed. |
|
# `isTranscriptMode:X,verbose:Y` → `isTranscriptMode:1...1,verbose:1...1` |
|
# |
|
# The API-side fix (getting Opus 4.7 to return summarized thinking text |
|
# instead of empty blocks) is NOT done here — use the hidden CLI flag: |
|
# claude --thinking-display summarized |
|
# or wrap it in `~/.claude/local/claude` so every launch gets it: |
|
# #!/bin/bash |
|
# exec "$HOME/.claude/local/node_modules/.bin/claude" \ |
|
# --thinking-display summarized "$@" |
|
# |
|
# Re-run after every `claude update`. If Anthropic's minifier assigns |
|
# different variable names in a future release the regex may stop matching; |
|
# the script will refuse to touch the binary and tell you so. |
|
# |
|
# Requirements: macOS (uses `codesign`, BSD `stat -f%z`, BSD `md5 -q`). |
|
# A Linux port would need `stat -c%s`, `md5sum`, and to drop the codesign |
|
# step (Linux Mach-O doesn't exist; claude.exe on Linux is ELF and signed |
|
# differently). Not tested on Linux. |
|
|
|
set -euo pipefail |
|
|
|
CLI_PATH="$HOME/.claude/local/node_modules/@anthropic-ai/claude-code/bin/claude.exe" |
|
BACKUP_DIR="$HOME/.claude/backups" |
|
|
|
RED=$'\033[0;31m' |
|
GREEN=$'\033[0;32m' |
|
YELLOW=$'\033[1;33m' |
|
NC=$'\033[0m' |
|
|
|
echo "=== Claude Code Thinking Block Patcher (Mach-O) ===" |
|
echo "" |
|
|
|
if [ ! -f "$CLI_PATH" ]; then |
|
echo -e "${RED}Error: binary not found at $CLI_PATH${NC}" |
|
echo "Is Claude Code installed?" |
|
exit 1 |
|
fi |
|
|
|
if ! file "$CLI_PATH" | grep -q "Mach-O"; then |
|
echo -e "${RED}Error: $CLI_PATH is not a Mach-O binary.${NC}" |
|
echo "This script patches the 2.1.113+ native binary. On pre-native" |
|
echo "versions, the JS lives at the same path as cli.js and you want a" |
|
echo "sed-based patcher against that file instead." |
|
exit 1 |
|
fi |
|
|
|
count_re() { |
|
perl -0777 -ne 'BEGIN{$n=0} $n++ while /'"$1"'/g; END{print $n}' "$CLI_PATH" |
|
} |
|
|
|
ORIG_EARLY_RETURN='case"thinking":\{if\(![A-Za-z0-9_\$]+(?:&&![A-Za-z0-9_\$]+)+\)return null;' |
|
PATCHED_EARLY_RETURN='case"thinking":\{if\(!1+(?:&&![A-Za-z0-9_\$]+)+\)return null;' |
|
ORIG_TRANSCRIPT='createElement\([A-Za-z0-9_\$]+,\{addMargin:[A-Za-z0-9_\$]+,param:[A-Za-z0-9_\$]+,isTranscriptMode:[A-Za-z0-9_\$]+,verbose:[A-Za-z0-9_\$]+' |
|
PATCHED_TRANSCRIPT='createElement\([A-Za-z0-9_\$]+,\{addMargin:[A-Za-z0-9_\$]+,param:[A-Za-z0-9_\$]+,isTranscriptMode:1+,verbose:1+' |
|
|
|
n_orig_er=$(count_re "$ORIG_EARLY_RETURN") |
|
n_patched_er=$(count_re "$PATCHED_EARLY_RETURN") |
|
n_orig_ts=$(count_re "$ORIG_TRANSCRIPT") |
|
n_patched_ts=$(count_re "$PATCHED_TRANSCRIPT") |
|
|
|
if [ "$n_patched_er" -gt 0 ] && [ "$n_patched_ts" -gt 0 ] && [ "$n_orig_er" -eq 0 ]; then |
|
echo -e "${YELLOW}Already patched!${NC}" |
|
echo " Early-return neutralized: $n_patched_er instance(s)" |
|
echo " isTranscriptMode+verbose forced truthy: $n_patched_ts instance(s)" |
|
echo "" |
|
echo "To restore original: cp \"$BACKUP_DIR/claude.exe.backup.\"* \"$CLI_PATH\"" |
|
exit 0 |
|
fi |
|
|
|
if [ "$n_orig_er" -eq 0 ]; then |
|
echo -e "${RED}Error: could not find the thinking-block early-return pattern.${NC}" |
|
echo "Claude Code's minified structure likely changed. Patterns tried:" |
|
echo " $ORIG_EARLY_RETURN" |
|
exit 1 |
|
fi |
|
|
|
if [ "$n_orig_ts" -eq 0 ]; then |
|
echo -e "${RED}Error: could not find the thinking createElement pattern.${NC}" |
|
echo "Claude Code's minified structure likely changed." |
|
exit 1 |
|
fi |
|
|
|
mkdir -p "$BACKUP_DIR" |
|
BACKUP_FILE="$BACKUP_DIR/claude.exe.backup.$(md5 -q "$CLI_PATH" | cut -c1-8)" |
|
echo "Backing up original to: $BACKUP_FILE" |
|
cp "$CLI_PATH" "$BACKUP_FILE" |
|
|
|
SIZE_BEFORE=$(stat -f%z "$CLI_PATH") |
|
|
|
# Edit 1: neutralize thinking-block early return. |
|
# `!<var>` (1 + len(var) bytes) → `!1...1` (1 + len(var) "1"s). Same width. |
|
# !11...1 evaluates to false (11..1 is truthy non-zero number), short-circuits |
|
# the && chain, whole condition is false, `return null` never fires. |
|
perl -i -0777 -pe ' |
|
s{(case"thinking":\{if\()!([A-Za-z0-9_\$]+)}{$1 . "!" . ("1" x length($2))}ge |
|
' "$CLI_PATH" |
|
|
|
# Edit 2: force thinking createElement to render expanded by flipping |
|
# isTranscriptMode and verbose to truthy ones-digits. Both props are only |
|
# rebound HERE — other components keep their real verbose state, so tool |
|
# output stays collapsed. |
|
perl -i -0777 -pe ' |
|
s{(createElement\([A-Za-z0-9_\$]+,\{addMargin:[A-Za-z0-9_\$]+,param:[A-Za-z0-9_\$]+,isTranscriptMode:)([A-Za-z0-9_\$]+)(,verbose:)([A-Za-z0-9_\$]+)}{$1 . ("1" x length($2)) . $3 . ("1" x length($4))}ge |
|
' "$CLI_PATH" |
|
|
|
SIZE_AFTER=$(stat -f%z "$CLI_PATH") |
|
|
|
restore_and_die() { |
|
echo -e "${RED}$1${NC}" |
|
echo "Restoring from backup..." |
|
cp "$BACKUP_FILE" "$CLI_PATH" |
|
exit 1 |
|
} |
|
|
|
if [ "$SIZE_BEFORE" -ne "$SIZE_AFTER" ]; then |
|
restore_and_die "Length preservation violated: $SIZE_BEFORE → $SIZE_AFTER bytes." |
|
fi |
|
|
|
new_patched_er=$(count_re "$PATCHED_EARLY_RETURN") |
|
new_patched_ts=$(count_re "$PATCHED_TRANSCRIPT") |
|
|
|
if [ "$new_patched_er" -eq 0 ] || [ "$new_patched_ts" -eq 0 ]; then |
|
restore_and_die "Patches didn't land as expected (early-return=$new_patched_er, createElement=$new_patched_ts)." |
|
fi |
|
|
|
# Re-sign the Mach-O. Anthropic ships it with a Developer ID signature + hardened |
|
# runtime; any byte modification invalidates the signature and Apple Silicon's |
|
# kernel SIGKILLs the binary on exec ("Killed: 9"). An ad-hoc signature |
|
# (`--sign -`) is enough to get past that — macOS accepts ad-hoc signatures on |
|
# locally-modified binaries. |
|
echo "Re-signing binary with ad-hoc signature (required after modification)..." |
|
if ! codesign --force --sign - "$CLI_PATH" 2>&1; then |
|
restore_and_die "codesign failed." |
|
fi |
|
|
|
if ! "$CLI_PATH" --version >/dev/null 2>&1; then |
|
restore_and_die "Binary fails to execute after patching and re-signing." |
|
fi |
|
|
|
echo -e "${GREEN}Patch applied successfully.${NC}" |
|
echo " Size preserved: $SIZE_BEFORE bytes" |
|
echo " Early-return neutralized: $new_patched_er instance(s)" |
|
echo " isTranscriptMode+verbose forced truthy: $new_patched_ts instance(s)" |
|
echo "" |
|
echo "Thinking blocks will auto-expand inline on next Claude Code launch." |
|
echo "To restore original: cp \"$BACKUP_FILE\" \"$CLI_PATH\"" |