Skip to content

Instantly share code, notes, and snippets.

@Macmee
Last active April 3, 2026 04:34
Show Gist options
  • Select an option

  • Save Macmee/a9e117a7c1ebaadea24c171c1c834651 to your computer and use it in GitHub Desktop.

Select an option

Save Macmee/a9e117a7c1ebaadea24c171c1c834651 to your computer and use it in GitHub Desktop.
Enable auto mode in Claude Code with LiteLLM or any ANTHROPIC_BASE_URL proxy
#!/usr/bin/env python3
"""
patch-claude-auto-mode.py
Enables auto mode in Claude Code for plans that don't natively include it.
Two patches are applied:
1) Beta header: Claude Code sends an "afk-mode" beta header with API
requests when auto mode is active. The Anthropic API validates this
against the user's plan and returns HTTP 400 if the plan doesn't
include auto mode. We flip the `!` in `!arr.includes(hdr)` to a
space, inverting the condition so the header is never pushed.
2) Client-side gate: GrowthBook returns enabled="disabled" for plans
without auto mode. Gate checks compare enabledState against "disabled"
and block auto mode. We change the comparison literals to "disablez"
so they never match. The parser and default value are left intact.
Both patches are needed: (1) alone leaves the client gate blocking,
(2) alone leaves the server rejecting the request.
Usage:
./patch-claude-auto-mode.py # patch current claude binary
./patch-claude-auto-mode.py --verify # check if already patched
./patch-claude-auto-mode.py --revert # restore from backup
./patch-claude-auto-mode.py --debug # show all targets
After patching, launch claude with:
claude --enable-auto-mode --permission-mode auto
"""
import os
import shutil
import subprocess
import sys
import tempfile
GREEN = "\033[0;32m"
YELLOW = "\033[0;33m"
CYAN = "\033[0;36m"
RED = "\033[0;31m"
NC = "\033[0m"
def info(msg): print(f"{GREEN}[ok]{NC} {msg}")
def warn(msg): print(f"{YELLOW}[warn]{NC} {msg}")
def note(msg): print(f"{CYAN}[info]{NC} {msg}")
def fail(msg): print(f"{RED}[error]{NC} {msg}")
def find_claude_binary():
try:
which = subprocess.run(["which", "claude"], capture_output=True, text=True)
if which.returncode != 0:
return None
path = which.stdout.strip()
return os.path.realpath(path)
except Exception:
return None
def get_claude_version(binary_path):
try:
result = subprocess.run([binary_path, "--version"], capture_output=True, text=True, timeout=5)
return result.stdout.strip()
except Exception:
return "unknown"
# ============================================================================
# Patch 1: Beta header push prevention.
#
# The compiled code has: !ARRAY.includes(VAR))ARRAY.push(VAR)
# where VAR is the mangled name for AFK_MODE_BETA_HEADER ("afk-mode-...").
#
# We flip the `!` to ` ` (space). This turns:
# !arr.includes(hdr) -> true (header absent) -> push fires
# into:
# arr.includes(hdr) -> false (header absent) -> push skipped
#
# The variable name is extracted dynamically from the const definition
# VARNAME="afk-mode-YYYY-MM-DD", so this is version-agnostic.
# ============================================================================
import re
def find_beta_header_targets(data):
unpatched = []
already = []
# Find each const definition: VARNAME="afk-mode-YYYY-MM-DD"
for m in re.finditer(rb'(\w+)="afk-mode-\d{4}-\d{2}-\d{2}"', data):
varname = re.escape(m.group(1))
# Pattern: !ARR.includes(VAR))ARR.push(VAR)
pat = (rb'(!)(\w+\.includes\(' + varname +
rb'\))\2*\w*\.push\(' + varname + rb'\)')
# Simpler: just find !WORD.includes(VAR)) followed by push(VAR)
pat = (rb'!(\w+)\.includes\(' + varname +
rb'\)\)\1\.push\(' + varname + rb'\)')
for m2 in re.finditer(pat, data):
bang_pos = m2.start()
if data[bang_pos:bang_pos + 1] == b"!":
unpatched.append(bang_pos)
# (already-patched would be a space at that position)
# Detect already-patched: space where ! was
pat_done = (rb' (\w+)\.includes\(' + varname +
rb'\)\)\1\.push\(' + varname + rb'\)')
for m3 in re.finditer(pat_done, data):
already.append(m3.start())
return unpatched, already
# ============================================================================
# Patch 2: Client-side gate checks ("disabled" -> "disablez" in comparisons)
# ============================================================================
def _find_exclusion_zones(data):
"""Byte ranges NOT to patch: parser functions and default assignments."""
zones = []
disabled_str = b'"disabled"'
opt_in_str = b'"opt-in"'
enabled_str = b'"enabled"'
# Parser functions (contain all three strings within ~120 bytes)
pos = 0
while True:
pos = data.find(opt_in_str, pos)
if pos == -1: break
window_start = max(0, pos - 120)
window = data[window_start:pos + len(opt_in_str)]
if enabled_str in window and disabled_str in window:
zones.append((max(0, pos - 150), pos + 50))
pos += 1
# Default variable assignments: ="disabled" (not ==="disabled")
pos = 0
while True:
pos = data.find(disabled_str, pos)
if pos == -1: break
if pos >= 1:
before = data[max(0, pos - 3):pos]
if before.endswith(b"=") and not before.endswith(b"=="):
zones.append((pos, pos + len(disabled_str)))
pos += 1
return zones
def _in_zone(pos, zones):
for s, e in zones:
if s <= pos <= e:
return True
return False
def _is_patchable(data, pos):
if pos == 0: return True
before = data[pos - 1:pos]
return before not in (b" ", b"`")
def _scan(data, start, end, needle, patch_needle, results, patched, seen, zones):
region = data[start:end]
idx = 0
while True:
idx_d = region.find(needle, idx)
idx_p = region.find(patch_needle, idx)
if idx_d == -1 and idx_p == -1: break
if idx_d != -1 and (idx_p == -1 or idx_d <= idx_p):
abs_pos = start + idx_d
d_pos = abs_pos + 8
if (d_pos not in seen and data[d_pos:d_pos+1] == b"d"
and not _in_zone(abs_pos, zones)
and _is_patchable(data, abs_pos)):
results.append(d_pos)
seen.add(d_pos)
idx = idx_d + 1
else:
abs_pos = start + idx_p
z_pos = abs_pos + 8
if (z_pos not in seen and not _in_zone(abs_pos, zones)
and _is_patchable(data, abs_pos)):
patched.append(z_pos)
seen.add(z_pos)
idx = idx_p + 1
def find_gate_check_targets(data):
disabled_str = b'"disabled"'
patched_str = b'"disablez"'
unpatched, already = [], []
seen = set()
zones = _find_exclusion_zones(data)
# Strategy A: tengu_auto_mode_config anchor
anchor = b"tengu_auto_mode_config"
pos = 0
while True:
pos = data.find(anchor, pos)
if pos == -1: break
_scan(data, max(0, pos-4000), min(len(data), pos+2000),
disabled_str, patched_str, unpatched, already, seen, zones)
pos += 1
# Strategy B: setAutoModeCircuitBroken anchor
anchor2 = b"setAutoModeCircuitBroken"
pos = 0
while True:
pos = data.find(anchor2, pos)
if pos == -1: break
chunk = data[pos:pos+600]
if disabled_str in chunk or patched_str in chunk:
_scan(data, pos, pos+600, disabled_str, patched_str,
unpatched, already, seen, zones)
pos += 1
# Strategy C: command("auto-mode") anchor
anchor3 = b'command("auto-mode")'
pos = 0
while True:
pos = data.find(anchor3, pos)
if pos == -1: break
_scan(data, max(0, pos-200), min(len(data), pos+50),
disabled_str, patched_str, unpatched, already, seen, zones)
pos += 1
return unpatched, already
# ============================================================================
# Commands
# ============================================================================
def cmd_patch(binary_path):
with open(binary_path, "rb") as f:
data = bytearray(f.read())
beta_unp, beta_done = find_beta_header_targets(data)
gate_unp, gate_done = find_gate_check_targets(data)
n_beta = len(beta_unp)
n_gate = len(gate_unp)
n_beta_done = len(beta_done)
n_gate_done = len(gate_done)
if n_beta + n_gate + n_beta_done + n_gate_done == 0:
fail("No patch targets found. Is this Claude Code v2.1.85+?")
return 1
if n_beta + n_gate == 0:
info(f"Already patched ({n_beta_done} headers + {n_gate_done} gate checks)")
return 0
# Backup
backup = binary_path + ".pre-automode-patch"
if not os.path.exists(backup):
shutil.copy2(binary_path, backup)
note(f"Backup saved: {backup}")
# Apply patches
for p in beta_unp:
data[p] = ord(" ") # '!' -> ' ' prevents beta header push
for p in gate_unp:
data[p] = ord("z") # 'd' -> 'z' in "disabled" -> "disablez"
# Write
tmp_fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(binary_path))
try:
os.write(tmp_fd, data)
os.close(tmp_fd)
os.chmod(tmp_path, os.stat(binary_path).st_mode)
os.rename(tmp_path, binary_path)
except Exception:
os.unlink(tmp_path)
raise
info(f"Patched {n_beta} beta headers (nulled) + {n_gate} gate checks")
# Re-sign on macOS
if sys.platform == "darwin":
try:
subprocess.run(["codesign", "--force", "--sign", "-", binary_path],
capture_output=True, check=True)
info("Re-signed binary (macOS ad-hoc signature)")
except FileNotFoundError:
warn("codesign not found; may be killed by macOS Gatekeeper")
except subprocess.CalledProcessError as e:
warn(f"codesign failed: {e.stderr.decode().strip()}")
print()
info("Launch with: claude --enable-auto-mode --permission-mode auto")
print()
note(f"To undo: {sys.argv[0]} --revert")
note("Re-run after each Claude Code update.")
return 0
def cmd_verify(binary_path):
with open(binary_path, "rb") as f:
data = f.read()
beta_unp, beta_done = find_beta_header_targets(data)
gate_unp, gate_done = find_gate_check_targets(data)
n_unp = len(beta_unp) + len(gate_unp)
n_done = len(beta_done) + len(gate_done)
if n_unp == 0 and n_done == 0:
fail("No patch targets found.")
return 1
if n_unp == 0:
info(f"Fully patched ({len(beta_done)} headers + {len(gate_done)} gate checks)")
return 0
if n_done == 0:
warn(f"Not patched ({len(beta_unp)} headers + {len(gate_unp)} gate checks)")
else:
warn(f"Partial: {n_done} done, {n_unp} remaining")
return 1
def cmd_revert(binary_path):
backup = binary_path + ".pre-automode-patch"
if not os.path.exists(backup):
fail(f"No backup found at {backup}")
return 1
shutil.copy2(backup, binary_path)
info("Restored from backup")
return 0
def cmd_debug(binary_path):
with open(binary_path, "rb") as f:
data = f.read()
print("=== Beta header push targets ===")
beta_unp, beta_done = find_beta_header_targets(data)
print(f" Unpatched: {len(beta_unp)} Already patched: {len(beta_done)}")
for p in beta_unp:
ctx = data[max(0,p-20):p+60].decode("utf-8", errors="replace")
print(f" offset {p}: ...{ctx}...")
print("\n=== Gate check targets ===")
gate_unp, gate_done = find_gate_check_targets(data)
print(f" Unpatched: {len(gate_unp)} Already patched: {len(gate_done)}")
for p in sorted(gate_unp):
ctx = data[max(0,p-40):p+20].decode("utf-8", errors="replace")
print(f" offset {p}: ...{ctx}...")
return 0
def main():
mode = "patch"
if len(sys.argv) > 1:
flag = sys.argv[1]
if flag == "--verify": mode = "verify"
elif flag == "--revert": mode = "revert"
elif flag == "--debug": mode = "debug"
elif flag in ("-h", "--help"):
print(__doc__.strip())
return 0
else:
fail(f"Unknown flag: {flag}")
return 1
binary_path = find_claude_binary()
if not binary_path:
fail("Could not find claude binary. Is it installed?")
return 1
note(f"Binary: {binary_path}")
note(f"Version: {get_claude_version(binary_path)}")
print()
if mode == "verify": return cmd_verify(binary_path)
elif mode == "revert": return cmd_revert(binary_path)
elif mode == "debug": return cmd_debug(binary_path)
else: return cmd_patch(binary_path)
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment