Skip to content

Instantly share code, notes, and snippets.

@phpmypython
Created April 18, 2026 03:12
Show Gist options
  • Select an option

  • Save phpmypython/a3c0267742fd65d2ebf247a5dd87ea88 to your computer and use it in GitHub Desktop.

Select an option

Save phpmypython/a3c0267742fd65d2ebf247a5dd87ea88 to your computer and use it in GitHub Desktop.
Length-preserving Mach-O binary patcher that restores auto-expanded thinking blocks on Claude Code 2.1.113+ — the native-binary replacement for the old sed-on-cli.js patchers that stopped working when Anthropic switched to a Bun-compiled executable. macOS only. Backup + auto-restore on failure.

Claude Code thinking-block auto-expand patcher (2.1.113+ Mach-O)

Restores auto-expanded-inline thinking blocks in the Claude Code live view without forcing global --verbose (which also expands tool output and drowns the session).

Background

Up through ~2.1.110 Claude Code shipped a pure-JS cli.js that power users patched with sed to keep thinking blocks visible during work. Starting around 2.1.111 the package ships a Bun-compiled 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 — any length-changing edit corrupts the binary
  • The binary is Developer-ID signed with hardened runtime — any byte change invalidates the signature and Apple Silicon's kernel SIGKILLs it on exec (Killed: 9)

This script handles both: two length-preserving regex substitutions, then an ad-hoc re-sign with codesign --force --sign -.

What it patches

Two edits against the embedded minified JS:

  1. Neutralize the thinking-block early return in the live-view render path. case"thinking":{if(!X&&!Y)return null;case"thinking":{if(!1&&!Y)return null; (same bytes; !1 short-circuits the && chain to always-false so the return never fires).
  2. Force the thinking component into expanded render by flipping its isTranscriptMode and verbose props to truthy ones-digits only in that specific createElement call. Other components keep the real verbose state, so tool output stays collapsed.

Scope

This only covers the UI auto-expand. The API-side fix (getting Opus 4.7 to return summarized thinking text instead of empty blocks) is separately handled by the hidden --thinking-display summarized flag — no binary patching required, just wrap your launcher:

#!/bin/bash
exec "$HOME/.claude/local/node_modules/.bin/claude" --thinking-display summarized "$@"

Discovered by @alkautsarf, verified by @Nitjsefnie / @geraertsf / @ccclapp on issue #49268.

Requirements

  • macOS (uses codesign, BSD stat -f%z, BSD md5 -q). Apple Silicon tested.
  • Perl (ships with macOS).
  • Official @anthropic-ai/claude-code install at ~/.claude/local.

Linux port would need stat -c%s / md5sum and to drop the codesign step (ELF on Linux, signed differently). Not tested.

Usage

curl -o ~/.claude/scripts/patch-thinking-blocks-macho.sh <raw-gist-url>
chmod +x ~/.claude/scripts/patch-thinking-blocks-macho.sh
~/.claude/scripts/patch-thinking-blocks-macho.sh

Re-run after every claude update. If Anthropic's minifier reassigns variable names in a future release the regex may stop matching; the script will refuse to touch the binary and print the pattern it was looking for rather than bricking your install.

Safety

  • Backs up original to ~/.claude/backups/claude.exe.backup.<hash> before touching anything
  • Verifies length preservation, patch application count, re-signing, and a final --version integrity check
  • Auto-restores from backup on any failure
  • Idempotent: detects already-patched state and exits cleanly

Related

  • Issue: anthropics/claude-code#49268
  • Prior-art JS patcher (for pre-2.1.111): pattern matches case"thinking":{if(!X&&!Y)return null;, isTranscriptMode:X, and .display??void 0:void 0 in the minified cli.js
#!/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\""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment