Claude Code 综合补丁工具
- 内容介绍
- 文章标签
- 相关推荐
@Haleclipse 佬的 Claude Code StatusLine | 小工具 大用处!| 超绝更新自定义 ccline --patch 的 /chrome 解锁 与 Context Warning 禁用
还有 @user2996 佬的 Claude Code强制开启 Tool Search工具分享
都十分的好用,但是是 ccline 无法 patch bun 与 vsc 的 claude,于是我把两位佬的工具整合了一下,三个愿望一次满足
#!/usr/bin/env python3
"""
Claude Code 综合补丁脚本
补丁项:
1. ToolSearch 域名限制解除 — 允许 ToolSearch 访问任意域名
2. Chrome 订阅检查绕过 — /chrome 命令不再要求 claude.ai 订阅
3. Context Warning 禁用 — 禁用 context window 接近上限时的警告(不影响 auto-compact)
4. Auth conflict 警告抑制 — 因 Patch 2 导致的误触发 OBK 警告
5. Read/Search 折叠禁用 — 禁止 Read/Search 工具结果折叠
支持:
macOS / Windows / Linux
bun 官方二进制 / npm / pnpm / Homebrew / VS Code·Cursor 扩展
用法:
python patch-claude.py # 交互式
python patch-claude.py --auto # 自动补丁所有
python patch-claude.py --restore # 从备份恢复
python patch-claude.py --status # 仅查看状态
"""
import sys
import os
import re
import shutil
import platform
import subprocess
from pathlib import Path
from dataclasses import dataclass, field
from typing import Callable
# ── 补丁定义 ──────────────────────────────────────────────────────────
@dataclass
class PatchDef:
name: str
description: str
target_re: re.Pattern[bytes]
patched_re: re.Pattern[bytes]
build_replacement: Callable[[re.Match[bytes]], bytes]
default_enabled: bool = True
def _pad_to(prefix: bytes, suffix: bytes, length: int) -> bytes:
padding = length - len(prefix) - len(suffix)
if padding < 0:
raise ValueError(f"patch template too long for match length {length}")
return prefix + b" " * padding + suffix
# --- Patch 1: ToolSearch domain bypass ---
_TS_TARGET = re.compile(
rb'return\["api\.anthropic\.com"\]\.includes\('
rb'([A-Za-z_$][A-Za-z0-9_$]*)\)\}catch\{return!1\}'
)
_TS_PATCHED = re.compile(rb'return!0/\* *\*/\}catch\{return!0\}')
PATCH_TOOLSEARCH = PatchDef(
name="toolsearch",
description="ToolSearch 域名限制解除 (新版已支持 env ENABLE_TOOL_SEARCH 按需控制)",
target_re=_TS_TARGET,
patched_re=_TS_PATCHED,
build_replacement=lambda m: _pad_to(
b"return!0/*", b"*/}catch{return!0}", len(m.group(0))
),
default_enabled=False,
)
# --- Patch 2: Chrome subscription bypass ---
# Pattern A: function XX(){if(!YY())return!1;return ZZ(WW()?.scopes)} → return!0
# Pattern B: apiKey:XX()?null: → apiKey:!1 ?null: (keep API key for non-OAuth users)
_ID = rb'[A-Za-z_$][A-Za-z0-9_$]*'
_CH_TARGET = re.compile(
rb'function (' + _ID + rb')\(\)\{if\(!' + _ID + rb'\(\)\)return!1;return '
+ _ID + rb'\(' + _ID + rb'\(\)\?\.scopes\)\}'
rb'|'
rb'apiKey:' + _ID + rb'\(\)\?null:'
)
_CH_PATCHED = re.compile(
rb'function ' + _ID + rb'\(\)\{return!0 +\}'
rb'|'
rb'apiKey:!1 +\?null:'
)
def _ch_replace(m: re.Match[bytes]) -> bytes:
original = m.group(0)
if original.startswith(b'function '):
fn_name = m.group(1)
return _pad_to(b"function " + fn_name + b"(){return!0", b"}", len(original))
# apiKey:ID()?null: → apiKey:!1 ?null:
prefix = b'apiKey:'
suffix = b'?null:'
call = original[len(prefix):-len(suffix)]
padded = b'!1' + b' ' * (len(call) - 2)
return prefix + padded + suffix
PATCH_CHROME = PatchDef(
name="chrome",
description="Chrome 订阅检查绕过 (/chrome)",
target_re=_CH_TARGET,
patched_re=_CH_PATCHED,
build_replacement=_ch_replace,
)
# --- Patch 3: Context warning disable ---
# isAboveWarningThreshold:X,...}=Y,Z=FN();if(!X||Z)return null
_CW_TARGET = re.compile(
rb'isAboveWarningThreshold:(' + _ID + rb'),isAboveErrorThreshold:' + _ID
+ rb'\}=' + _ID + rb',(' + _ID + rb')=' + _ID + rb'\(\);if\(!\1\|\|\2\)return null'
)
_CW_PATCHED = re.compile(
rb'isAboveWarningThreshold:' + _ID + rb',isAboveErrorThreshold:' + _ID
+ rb'\}=' + _ID + rb',' + _ID + rb'=' + _ID + rb'\(\);if\(!0\|\|' + _ID + rb'\)return null'
)
def _cw_replace(m: re.Match[bytes]) -> bytes:
warn_var = m.group(1)
dismiss_var = m.group(2)
return m.group(0).replace(
b"if(!" + warn_var + b"||" + dismiss_var + b")",
b"if(!0||" + dismiss_var + b")",
1,
)
PATCH_CONTEXT_WARNING = PatchDef(
name="context_warning",
description="Context Warning 禁用",
target_re=_CW_TARGET,
patched_re=_CW_PATCHED,
build_replacement=_cw_replace,
)
# --- Patch 4: Auth conflict warning suppress ---
# r8() 被 patch 为 return!0 后,OBK 的 isActive 会误触发 auth conflict 警告
_AW_TARGET = re.compile(
rb'isActive:\(\)=>\{let (' + _ID + rb')=' + _ID + rb'\(\);return '
+ _ID + rb'\(\)&&\(\1\.source==="ANTHROPIC_AUTH_TOKEN"\|\|\1\.source==="apiKeyHelper"\)\}'
)
_AW_PATCHED = re.compile(
rb'isActive:\(\)=>\{return!1 +\}'
)
PATCH_AUTH_WARNING = PatchDef(
name="auth_warning",
description="Auth conflict 警告抑制",
target_re=_AW_TARGET,
patched_re=_AW_PATCHED,
build_replacement=lambda m: _pad_to(
b"isActive:()=>{return!1", b"}", len(m.group(0))
),
)
# --- Patch 5: Collapse Read/Search disable ---
# <=2.1.87: isCollapsible:XX.isSearch||XX.isRead||!1
# >=2.1.88: isCollapsible:T||(B9()?H===Aq:!1)
_CRS_TARGET = re.compile(
rb'isCollapsible:(' + _ID + rb')\.isSearch\|\|\1\.isRead\|\|!1'
rb'|'
rb'isCollapsible:' + _ID + rb'\|\|\(' + _ID + rb'\(\)\?' + _ID + rb'===' + _ID + rb':!1\)'
)
_CRS_PATCHED = re.compile(
rb'isCollapsible:!1/\* *\*/'
)
PATCH_COLLAPSE_RS = PatchDef(
name="collapse_read_search",
description="Read/Search 折叠禁用",
target_re=_CRS_TARGET,
patched_re=_CRS_PATCHED,
build_replacement=lambda m: _pad_to(
b"isCollapsible:!1/*", b"*/", len(m.group(0))
),
)
ALL_PATCHES = [PATCH_TOOLSEARCH, PATCH_CHROME, PATCH_CONTEXT_WARNING, PATCH_AUTH_WARNING, PATCH_COLLAPSE_RS]
BACKUP_SUFFIX = ".claude-patch-bak"
# ── 补丁引擎 ──────────────────────────────────────────────────────────
def get_patch_status(data: bytes, patch: PatchDef) -> str:
"""Return 'unpatched' / 'patched' / 'unknown' for a single patch."""
if patch.target_re.search(data):
return "unpatched"
if patch.patched_re.search(data):
return "patched"
return "unknown"
def apply_single_patch(data: bytes, patch: PatchDef) -> tuple[bytes, int]:
count = 0
def replacer(m: re.Match[bytes]) -> bytes:
nonlocal count
replacement = patch.build_replacement(m)
if len(replacement) != len(m.group(0)):
raise ValueError(
f"[{patch.name}] replacement length mismatch: "
f"{len(replacement)} != {len(m.group(0))}"
)
count += 1
return replacement
return patch.target_re.sub(replacer, data), count
def get_all_statuses(
data: bytes, patches: list[PatchDef] | None = None
) -> dict[str, str]:
targets = patches if patches is not None else ALL_PATCHES
return {p.name: get_patch_status(data, p) for p in targets}
def apply_all_patches(
data: bytes, patches: list[PatchDef] | None = None
) -> tuple[bytes, dict[str, int]]:
targets = patches if patches is not None else ALL_PATCHES
results: dict[str, int] = {}
for patch in targets:
data, count = apply_single_patch(data, patch)
results[patch.name] = count
return data, results
# ── 平台 & 工具 ──────────────────────────────────────────────────────
SYSTEM = platform.system()
IS_WINDOWS = (
SYSTEM == "Windows"
or "MSYS" in os.environ.get("MSYSTEM", "")
or "MINGW" in platform.platform()
)
def home() -> Path:
return Path.home()
def run_cmd(cmd: list[str], fallback: str = "") -> str:
if not shutil.which(cmd[0]):
return fallback
try:
r = subprocess.run(
cmd, capture_output=True, text=True, timeout=5, shell=IS_WINDOWS
)
return r.stdout.strip() if r.returncode == 0 else fallback
except Exception:
return fallback
def resolve_patch_target(path: Path) -> Path:
try:
return path.resolve(strict=True)
except OSError:
return path
def resign_if_needed(path: Path) -> tuple[bool, str]:
if SYSTEM != "Darwin":
return True, ""
result = subprocess.run(
["codesign", "--force", "--sign", "-", str(path)],
capture_output=True,
text=True,
)
if result.returncode == 0:
return True, "已完成 macOS ad-hoc 重签名。"
message = (result.stderr or result.stdout).strip() or "codesign 执行失败"
return False, message
# ── 安装探测 ──────────────────────────────────────────────────────────
class Installation:
def __init__(self, kind: str, target: Path, description: str):
self.kind = kind
self.source = target
self.target = resolve_patch_target(target)
self.description = description
self.backup = self.target.parent / (self.target.name + BACKUP_SUFFIX)
def __repr__(self):
return f"[{self.kind}] {self.description}\n 文件: {self.target}"
def _find_patch_target_in_pkg(pkg_dir: Path) -> Path | None:
cli_js = pkg_dir / "cli.js"
if cli_js.is_file() and b"api.anthropic.com" in cli_js.read_bytes():
return cli_js
for js_file in sorted(pkg_dir.rglob("*.js")):
if js_file.stat().st_size < 1000:
continue
try:
if b"api.anthropic.com" in js_file.read_bytes():
return js_file
except OSError:
continue
return None
def find_bun_installations() -> list[Installation]:
results = []
candidates: list[Path] = []
if IS_WINDOWS:
candidates.append(home() / ".local" / "bin" / "claude.exe")
else:
candidates.append(home() / ".claude" / "local" / "claude")
candidates.append(home() / ".local" / "bin" / "claude")
for p in candidates:
if p.is_file():
results.append(Installation("bun", p, f"Bun 官方安装 ({p})"))
return results
def find_npm_installations() -> list[Installation]:
results = []
npm_root = run_cmd(["npm", "root", "-g"])
if npm_root:
pkg_dir = Path(npm_root) / "@anthropic-ai" / "claude-code"
if pkg_dir.is_dir():
target = _find_patch_target_in_pkg(pkg_dir)
if target:
results.append(
Installation("npm", target, f"npm 全局安装 ({target})")
)
else:
results.extend(_find_npm_fallback())
return results
def _find_npm_fallback() -> list[Installation]:
h = home()
search_dirs: list[tuple[Path, str]] = []
if IS_WINDOWS:
appdata = Path(os.environ.get("APPDATA", ""))
if appdata.name:
search_dirs.append((appdata / "npm" / "node_modules", "npm 默认全局"))
nvm_home = os.environ.get(
"NVM_HOME", str(appdata / "nvm") if appdata.name else ""
)
if nvm_home:
nvm_path = Path(nvm_home)
if nvm_path.is_dir():
for d in nvm_path.iterdir():
nm = d / "node_modules"
if d.is_dir() and nm.is_dir():
search_dirs.append((nm, f"nvm ({d.name})"))
fnm_dir = Path(os.environ.get("FNM_DIR", str(h / ".fnm")))
nv = fnm_dir / "node-versions"
if nv.is_dir():
for d in nv.iterdir():
nm = d / "installation" / "node_modules"
if nm.is_dir():
search_dirs.append((nm, f"fnm ({d.name})"))
else:
nvm_dir = Path(os.environ.get("NVM_DIR", str(h / ".nvm")))
versions = nvm_dir / "versions" / "node"
if versions.is_dir():
for d in versions.iterdir():
nm = d / "lib" / "node_modules"
if nm.is_dir():
search_dirs.append((nm, f"nvm ({d.name})"))
fnm_dir = Path(os.environ.get("FNM_DIR", str(h / ".fnm")))
nv = fnm_dir / "node-versions"
if nv.is_dir():
for d in nv.iterdir():
nm = d / "installation" / "lib" / "node_modules"
if nm.is_dir():
search_dirs.append((nm, f"fnm ({d.name})"))
for p in [
Path("/usr/local/lib/node_modules"),
Path("/usr/lib/node_modules"),
]:
if p.is_dir():
search_dirs.append((p, "系统 npm"))
volta_home = Path(os.environ.get("VOLTA_HOME", str(h / ".volta")))
volta_node = volta_home / "tools" / "image" / "node"
if volta_node.is_dir():
for d in volta_node.iterdir():
nm = (d / "node_modules") if IS_WINDOWS else (d / "lib" / "node_modules")
if nm.is_dir():
search_dirs.append((nm, f"volta ({d.name})"))
results: list[Installation] = []
seen: set[str] = set()
for nm_dir, desc in search_dirs:
pkg_dir = nm_dir / "@anthropic-ai" / "claude-code"
if pkg_dir.is_dir():
target = _find_patch_target_in_pkg(pkg_dir)
if target:
key = str(target.resolve())
if key not in seen:
seen.add(key)
results.append(
Installation("npm", target, f"npm ({desc}) ({target})")
)
return results
def find_pnpm_installations() -> list[Installation]:
results = []
pnpm_root = run_cmd(["pnpm", "root", "-g"])
if not pnpm_root:
return results
pkg_dir = Path(pnpm_root) / "@anthropic-ai" / "claude-code"
if pkg_dir.is_dir():
target = _find_patch_target_in_pkg(pkg_dir)
if target:
results.append(
Installation(
"pnpm", target.resolve(), f"pnpm 全局安装 ({target.resolve()})"
)
)
return results
pnpm_dir = Path(pnpm_root).parent / ".pnpm"
if pnpm_dir.is_dir():
for pkg in pnpm_dir.rglob("@anthropic-ai/claude-code"):
if pkg.is_dir():
target = _find_patch_target_in_pkg(pkg)
if target:
results.append(
Installation("pnpm", target, f"pnpm 全局安装 ({target})")
)
break
return results
def find_brew_installations() -> list[Installation]:
if IS_WINDOWS:
return []
results = []
seen: set[str] = set()
brew_prefix = run_cmd(["brew", "--prefix"])
prefixes: list[Path] = []
if brew_prefix:
prefixes.append(Path(brew_prefix))
for fallback in [Path("/opt/homebrew"), Path("/usr/local")]:
if fallback not in prefixes and fallback.is_dir():
prefixes.append(fallback)
for prefix in prefixes:
caskroom = prefix / "Caskroom" / "claude-code"
if not caskroom.is_dir():
continue
for version_dir in sorted(caskroom.iterdir(), reverse=True):
if not version_dir.is_dir() or version_dir.name.startswith("."):
continue
binary = version_dir / "claude"
if binary.is_file() and binary.stat().st_size > 10 * 1024 * 1024:
key = str(binary.resolve())
if key not in seen:
seen.add(key)
results.append(
Installation(
"brew", binary, f"Homebrew cask ({version_dir.name})"
)
)
return results
def find_vscode_installations() -> list[Installation]:
results = []
search_bases = [
("vscode", "VS Code", home() / ".vscode" / "extensions"),
("vscode", "VS Code Insiders", home() / ".vscode-insiders" / "extensions"),
("cursor", "Cursor", home() / ".cursor" / "extensions"),
]
for kind, label, base in search_bases:
if not base.is_dir():
continue
ext_dirs = sorted(base.glob("anthropic.claude-code-*"), reverse=True)
if not ext_dirs:
continue
ext_dir = ext_dirs[0]
for name in ["claude.exe", "claude"] if IS_WINDOWS else ["claude"]:
for p in ext_dir.rglob(name):
if (
p.is_file()
and not p.name.endswith(".bak")
and p.stat().st_size > 10 * 1024 * 1024
):
results.append(
Installation(
kind, p, f"{label} 捆绑二进制 ({ext_dir.name})"
)
)
return results
def find_wsl_installations() -> list[Installation]:
if not IS_WINDOWS:
return []
distro_names = _get_wsl_distros()
if not distro_names:
return []
def _safe_is_dir(p: Path) -> bool:
try:
return p.is_dir()
except OSError:
return False
results: list[Installation] = []
for dname in distro_names:
distro = Path(f"//wsl.localhost/{dname}")
if not _safe_is_dir(distro):
continue
user_dirs: list[Path] = []
home_base = distro / "home"
if _safe_is_dir(home_base):
try:
user_dirs.extend(d for d in home_base.iterdir() if d.is_dir())
except OSError:
pass
root_home = distro / "root"
if _safe_is_dir(root_home):
user_dirs.append(root_home)
for udir in user_dirs:
versions_dir = udir / ".local" / "share" / "claude" / "versions"
if _safe_is_dir(versions_dir):
try:
for v in sorted(versions_dir.iterdir(), reverse=True):
if v.name.endswith(BACKUP_SUFFIX):
continue
if v.is_file() and v.stat().st_size > 10 * 1024 * 1024:
results.append(
Installation(
"wsl", v, f"WSL ({dname}) bun v{v.name}"
)
)
except OSError:
pass
nm_search: list[tuple[Path, str]] = []
nvm_node = udir / ".nvm" / "versions" / "node"
if _safe_is_dir(nvm_node):
try:
for v in nvm_node.iterdir():
nm = v / "lib" / "node_modules"
if v.is_dir() and nm.is_dir():
nm_search.append((nm, f"nvm {v.name}"))
except OSError:
pass
fnm_nv = udir / ".fnm" / "node-versions"
if _safe_is_dir(fnm_nv):
try:
for v in fnm_nv.iterdir():
nm = v / "installation" / "lib" / "node_modules"
if v.is_dir() and nm.is_dir():
nm_search.append((nm, f"fnm {v.name}"))
except OSError:
pass
volta_node = udir / ".volta" / "tools" / "image" / "node"
if _safe_is_dir(volta_node):
try:
for v in volta_node.iterdir():
nm = v / "lib" / "node_modules"
if v.is_dir() and nm.is_dir():
nm_search.append((nm, f"volta {v.name}"))
except OSError:
pass
for nm_dir, label in nm_search:
pkg = nm_dir / "@anthropic-ai" / "claude-code"
if _safe_is_dir(pkg):
target = _find_patch_target_in_pkg(pkg)
if target:
results.append(
Installation("wsl", target, f"WSL ({dname}) {label}")
)
for sys_nm in [
distro / "usr" / "local" / "lib" / "node_modules",
distro / "usr" / "lib" / "node_modules",
]:
pkg = sys_nm / "@anthropic-ai" / "claude-code"
if _safe_is_dir(pkg):
target = _find_patch_target_in_pkg(pkg)
if target:
results.append(
Installation("wsl", target, f"WSL ({dname}) npm 系统")
)
return results
def _get_wsl_distros() -> list[str]:
try:
r = subprocess.run(
["wsl.exe", "--list", "--quiet"], capture_output=True, timeout=5
)
if r.returncode != 0:
return []
text = r.stdout.decode("utf-16-le", errors="ignore").strip()
return [line.strip() for line in text.splitlines() if line.strip()]
except Exception:
return []
def find_all_installations() -> list[Installation]:
all_inst: list[Installation] = []
all_inst.extend(find_brew_installations())
all_inst.extend(find_bun_installations())
all_inst.extend(find_npm_installations())
all_inst.extend(find_pnpm_installations())
all_inst.extend(find_vscode_installations())
all_inst.extend(find_wsl_installations())
seen: set[str] = set()
deduped: list[Installation] = []
for inst in all_inst:
key = str(inst.target)
if key not in seen:
seen.add(key)
deduped.append(inst)
return deduped
# ── 补丁操作 ──────────────────────────────────────────────────────────
STATUS_SYMBOL = {"unpatched": "○", "patched": "●", "unknown": "?"}
STATUS_LABEL = {"unpatched": "未补丁", "patched": "已补丁", "unknown": "不兼容"}
def _write_via_rename(target: Path, data: bytes) -> bool:
tmp_path = target.with_suffix(target.suffix + ".tmp")
old_path = target.with_suffix(target.suffix + ".old")
for p in (tmp_path, old_path):
try:
p.unlink(missing_ok=True)
except OSError:
pass
tmp_path.write_bytes(data)
try:
target.rename(old_path)
except OSError:
tmp_path.unlink(missing_ok=True)
print(f" ✗ 无法重命名 {target.name},请关闭 claude 后重试。")
return False
tmp_path.rename(target)
try:
old_path.unlink(missing_ok=True)
except OSError:
pass
return True
def apply_patch(
inst: Installation, patches: list[PatchDef] | None = None
) -> bool:
targets = patches if patches is not None else ALL_PATCHES
data = inst.target.read_bytes()
statuses = get_all_statuses(data, targets)
has_unpatched = any(s == "unpatched" for s in statuses.values())
all_patched = all(s == "patched" for s in statuses.values())
if all_patched:
print(" ✓ 所有补丁已应用,跳过。")
return True
if not has_unpatched:
unknown = [
p.description
for p in targets
if statuses[p.name] == "unknown"
]
if unknown:
print(f" ✗ 以下补丁目标未找到(版本不兼容):")
for desc in unknown:
print(f" - {desc}")
return False
patched_data, counts = apply_all_patches(data, targets)
total = sum(counts.values())
if total == 0:
print(" ✗ 未找到任何可补丁的目标。")
return False
# backup
if not inst.backup.is_file():
shutil.copy2(inst.target, inst.backup)
print(f" 已备份到 {inst.backup}")
try:
inst.target.write_bytes(patched_data)
except PermissionError:
print(" 文件被占用,使用重命名方式替换...")
if not _write_via_rename(inst.target, patched_data):
return False
ok, message = resign_if_needed(inst.target)
if not ok:
print(f" ✗ 补丁已写入,但重签名失败: {message}")
return False
if message:
print(f" {message}")
for patch in targets:
c = counts[patch.name]
status = statuses[patch.name]
if c > 0:
print(f" ✓ {patch.description}: 替换 {c} 处")
elif status == "patched":
print(f" - {patch.description}: 已是最新")
else:
print(f" ✗ {patch.description}: 未找到目标")
return True
def restore_backup(inst: Installation) -> bool:
if not inst.backup.is_file():
print(f" ✗ 未找到备份文件 {inst.backup}")
return False
backup_data = inst.backup.read_bytes()
try:
inst.target.write_bytes(backup_data)
except PermissionError:
print(" 文件被占用,使用重命名方式替换...")
if not _write_via_rename(inst.target, backup_data):
return False
ok, message = resign_if_needed(inst.target)
if not ok:
print(f" ✗ 已恢复,但重签名失败: {message}")
return False
if message:
print(f" {message}")
print(" ✓ 已从备份恢复。")
return True
def print_status(
inst: Installation, patches: list[PatchDef] | None = None
):
targets = patches if patches is not None else ALL_PATCHES
data = inst.target.read_bytes()
statuses = get_all_statuses(data, targets)
has_backup = "有备份" if inst.backup.is_file() else "无备份"
print(f" {inst} | {has_backup}")
for patch in targets:
s = statuses[patch.name]
print(f" {STATUS_SYMBOL[s]} {patch.description}: {STATUS_LABEL[s]}")
print()
# ── 交互式 TUI ───────────────────────────────────────────────────────
# ANSI colors
_C_RESET = "\033[0m"
_C_BOLD = "\033[1m"
_C_GREEN = "\033[32m"
_C_YELLOW = "\033[33m"
_C_RED = "\033[31m"
_C_CYAN = "\033[36m"
_C_DIM = "\033[2m"
STATUS_COLOR = {"unpatched": _C_YELLOW, "patched": _C_GREEN, "unknown": _C_RED}
_PATCH_LETTERS = "abcdefghijklmnopqrstuvwxyz"
def _clear_screen():
print("\033[2J\033[H", end="", flush=True)
def _read_char() -> str:
"""Read a single character without waiting for Enter (raw mode)."""
if IS_WINDOWS:
import msvcrt
ch = msvcrt.getwch()
return ch
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
return ch
def _draw_tui(
installations: list[Installation],
enabled: list[bool],
mode: str,
inst_data: list[bytes],
):
action = "补丁" if mode == "patch" else "恢复"
max_letter = _PATCH_LETTERS[len(ALL_PATCHES) - 1]
print(f"{_C_BOLD}{'=' * 60}")
print(" Claude Code 综合补丁工具")
print(f"{'=' * 60}{_C_RESET}")
print(f" 系统: {SYSTEM} | Python {platform.python_version()}")
print()
# patch toggles
print(f"补丁项 ({_C_CYAN}a-{max_letter} 切换{_C_RESET}):")
for i, patch in enumerate(ALL_PATCHES):
letter = _PATCH_LETTERS[i]
if enabled[i]:
mark = f"{_C_GREEN}x{_C_RESET}"
else:
mark = " "
# dim styling for non-default disabled patches
if not patch.default_enabled and not enabled[i]:
desc = f"{_C_DIM}{patch.description}{_C_RESET}"
else:
desc = patch.description
print(f" {letter}. [{mark}] {desc}")
print()
# legend
print(
f" {_C_GREEN}●{_C_RESET} 已补丁 "
f"{_C_YELLOW}○{_C_RESET} 未补丁 "
f"{_C_RED}?{_C_RESET} 不兼容 "
f"{_C_DIM}|{_C_RESET} "
f"{_C_YELLOW}黄色{_C_RESET}=将处理 "
f"{_C_GREEN}绿色{_C_RESET}=已处理 "
f"{_C_DIM}灰色=跳过{_C_RESET}"
)
print()
# installations — always check ALL patches
n = len(installations)
print(f"检测到 {n} 个安装:")
print()
for i, inst in enumerate(installations, 1):
data = inst_data[i - 1]
has_backup = "有备份" if inst.backup.is_file() else "无备份"
indicators: list[str] = []
for j, patch in enumerate(ALL_PATCHES):
letter = _PATCH_LETTERS[j]
s = get_patch_status(data, patch)
symbol = STATUS_SYMBOL[s]
# each cell: "S L" padded to 5 visible chars ("● a ")
visible_text = f"{symbol} {letter}"
padding = " " * (5 - len(visible_text))
if enabled[j]:
color = STATUS_COLOR[s]
indicators.append(f"{color}{visible_text}{_C_RESET}{padding}")
else:
indicators.append(f"{_C_DIM}{visible_text}{_C_RESET}{padding}")
status_line = "".join(indicators)
print(f" {i}. [{inst.kind:6s}] {status_line}| {has_backup}")
print(f" {_C_DIM}{inst.target}{_C_RESET}")
print()
if n > 1:
print(f" 0. 全部{action}")
print()
# prompt
num_range = f"1-{n}" if n > 1 else "1"
zero_hint = " | 0 全部" if n > 1 else ""
print(
f"{_C_CYAN}按 a-{max_letter} 切换补丁项 | "
f"{num_range} {action}指定目标{zero_hint} | q 退出{_C_RESET}",
flush=True,
)
def interactive_tui(installations: list[Installation], mode: str = "patch"):
enabled = [p.default_enabled for p in ALL_PATCHES]
inst_data = [inst.target.read_bytes() for inst in installations]
_clear_screen()
_draw_tui(installations, enabled, mode, inst_data)
while True:
try:
ch = _read_char()
except (EOFError, KeyboardInterrupt):
print("\n已取消。")
return
choice = ch.lower()
if choice in ("\x03", "\x1b"): # Ctrl-C / Esc
print("\n已取消。")
return
if choice == "q":
print("\n已取消。")
return
# toggle patch: a-z
if choice in _PATCH_LETTERS:
idx = _PATCH_LETTERS.index(choice)
if idx < len(ALL_PATCHES):
enabled[idx] = not enabled[idx]
_clear_screen()
_draw_tui(installations, enabled, mode, inst_data)
continue
# select installation: number
if choice.isdigit():
num = int(choice)
n = len(installations)
if num == 0 and n > 1:
target_indices = list(range(n))
elif 1 <= num <= n:
target_indices = [num - 1]
else:
continue
selected = [p for p, e in zip(ALL_PATCHES, enabled) if e]
if not selected:
print(f"\n{_C_RED}未选择任何补丁项。{_C_RESET}")
continue
for idx in target_indices:
inst = installations[idx]
print(f"\n→ 处理: {inst}")
if mode == "patch":
apply_patch(inst, selected)
else:
restore_backup(inst)
print(f"\n{_C_GREEN}完成。重启 claude 生效。{_C_RESET}")
return
def auto_mode(
installations: list[Installation],
mode: str = "patch",
patches: list[PatchDef] | None = None,
):
if patches is None:
# --auto mode: only apply default_enabled patches
patches = [p for p in ALL_PATCHES if p.default_enabled]
for inst in installations:
print(f"\n→ 处理: {inst}")
if mode == "patch":
apply_patch(inst, patches)
else:
restore_backup(inst)
print("\n完成。重启 claude 生效。")
# ── 入口 ──────────────────────────────────────────────────────────────
def main():
mode = "patch"
auto = False
status_only = False
for arg in sys.argv[1:]:
if arg == "--restore":
mode = "restore"
elif arg == "--auto":
auto = True
elif arg == "--status":
status_only = True
is_interactive = not auto and not status_only
def _print_header():
print(f"{_C_BOLD}{'=' * 60}")
print(" Claude Code 综合补丁工具")
print(f"{'=' * 60}{_C_RESET}")
print(f" 系统: {SYSTEM} | Python {platform.python_version()}")
print()
# interactive mode: show scanning on a clean screen, TUI will redraw
if is_interactive:
_clear_screen()
_print_header()
print("正在扫描 Claude Code 安装...", flush=True)
else:
_print_header()
print("正在扫描 Claude Code 安装...")
installations = find_all_installations()
if not installations:
print(f"\n{_C_RED}未检测到任何 Claude Code 安装。{_C_RESET}")
print("支持: bun / npm -g / pnpm / Homebrew / VS Code·Cursor")
sys.exit(1)
if status_only:
print(f"\n检测到 {len(installations)} 个安装:\n")
for inst in installations:
print_status(inst)
_pause_if_needed(auto)
return
if auto:
auto_mode(installations, mode)
else:
interactive_tui(installations, mode)
_pause_if_needed(auto)
def _pause_if_needed(auto: bool):
if auto:
return
try:
input("\n按回车键退出...")
except (EOFError, KeyboardInterrupt):
pass
def _tui_demo():
"""Render _draw_tui with mock data for visual verification."""
from pathlib import Path
import tempfile, re
# create mock PatchDef objects
class MockPatch:
def __init__(self, name, description, default_enabled=True):
self.name = name
self.description = description
self.default_enabled = default_enabled
self.target_re = re.compile(rb"NEVER_MATCH_TARGET")
self.patched_re = re.compile(rb"NEVER_MATCH_PATCHED")
self.build_replacement = lambda m: m.group(0)
# override ALL_PATCHES temporarily
global ALL_PATCHES
saved = ALL_PATCHES
ALL_PATCHES = [
MockPatch("toolsearch", "ToolSearch 全域", default_enabled=False),
MockPatch("chrome", "Chrome 订阅绕过"),
MockPatch("context_warning", "Context Warning 禁用"),
MockPatch("auth_warning", "Auth conflict 警告抑制"),
MockPatch("collapse_rs", "Read/Search 折叠禁用"),
]
# create mock installations with temp files
tmp = Path(tempfile.mkdtemp())
mock_files = []
for name in ["cli.js", "bundle.js"]:
p = tmp / name
p.write_bytes(b"dummy")
mock_files.append(p)
class MockInstallation:
def __init__(self, kind, target, description):
self.kind = kind
self.target = target
self.description = description
self.backup = target.parent / (target.name + ".bak")
insts = [
MockInstallation("bun", mock_files[0], "Bun 官方安装"),
MockInstallation("vscode", mock_files[1], "VS Code 扩展"),
]
# mock data: all "unknown" since patterns won't match
inst_data = [f.read_bytes() for f in mock_files]
# enabled: first patch (toolsearch) disabled, rest enabled
enabled = [False, True, True, True, True]
print("\n=== TUI Demo (all statuses = unknown) ===\n")
_draw_tui(insts, enabled, "patch", inst_data)
# now test with patched_re matching to simulate "patched" status
ALL_PATCHES[1].patched_re = re.compile(rb"dummy") # chrome -> patched
ALL_PATCHES[2].target_re = re.compile(rb"dummy") # context_warning -> unpatched
# auth_warning stays unknown, collapse_rs stays unknown
inst_data = [f.read_bytes() for f in mock_files]
print("\n=== TUI Demo (mixed statuses) ===\n")
_draw_tui(insts, enabled, "patch", inst_data)
# cleanup
ALL_PATCHES = saved
for f in mock_files:
f.unlink()
tmp.rmdir()
if __name__ == "__main__":
import sys as _sys
if "--tui-demo" in _sys.argv:
_tui_demo()
else:
main()
保存为 patch-claude.py 后 python3 patch-claude.py 执行
image972×678 44.5 KB
image2348×1714 275 KB
试了下没啥问题,可以正常使用,感谢两位佬的付出
感谢楼里佬提的优化建议
优化了一下 TUI,支持选择补丁项目,默认不选中 ToolSearch
然后把哈雷佬的 关于 2.1.20 上线的 collapsed read/search groups
也集成进去了,代码已经更新到上面的 python 代码块中
image1910×1358 323 KB
网友解答:--【壹】--:
mark一下,明天看看,感谢大佬分享
--【贰】--: 哈雷彗星:
不过有1M了感觉开这个有点脱裤子放屁 我花十几k还吃不下这坨基础token么 离20%都好远(ノ_ _)ノ
这是幸福的烦恼
我用量不多用的 Pro 然后用 sub2api 保活,所以正好没有 1M 上下文并且是通过 API 调用,还是比较刚需的
不过现在能用 env 控制了确实没有必要特地 patch Tool Search 了
--【叁】--:
mark了,一会儿试试
--【肆】--:
先mark下, 免得明天找不到了
--【伍】--:
明天试试
--【陆】--:
mark 一下,明天试试
--【柒】--:
mark.稍后再看
--【捌】--:
原来如此
--【玖】--:
这么强!
--【拾】--:
就两个版本的事吧 就落盘了 灰度特性就是这样的
没放出来是没想清楚怎么调整
后来的逻辑是 非 api.anthropic.com 默认是false 因为本就不能保证第三方能够支持这个
所以只要用三方的话 只需要 加ENV控制打开即可(想开的情况下)
不过有1M了感觉开这个有点脱裤子放屁 我花十几k还吃不下这坨基础token么 离20%都好远(ノ_ _)ノ
--【拾壹】--:
好快的进场,刚要预言你的到来
其实略有神秘
--【拾贰】--:
不标记了,直接运行
image503×162 4.29 KB
--【拾叁】--:
是得下载好 CCometixLine执行这个脚本才有意义吗?
--【拾肆】--:
是因为 1M 上下文还是因为 cc 官方修了
--【拾伍】--:
看了眼,好恐怖一哈雷
--【拾陆】--:
明天试试看
--【拾柒】--:
哦,好像是 ENABLE_TOOL_SEARCH 这个 env 能直接控制了
--【拾捌】--:
先mark下, 免得明天找不到了
--【拾玖】--:
不是 ToolSearch已经不再需要补丁了w
还有就是 VCC的话 直接把native-binary 文件夹删了 再放置 claude-code 的npm包就是一样用了 bun版狗都不用
@Haleclipse 佬的 Claude Code StatusLine | 小工具 大用处!| 超绝更新自定义 ccline --patch 的 /chrome 解锁 与 Context Warning 禁用
还有 @user2996 佬的 Claude Code强制开启 Tool Search工具分享
都十分的好用,但是是 ccline 无法 patch bun 与 vsc 的 claude,于是我把两位佬的工具整合了一下,三个愿望一次满足
#!/usr/bin/env python3
"""
Claude Code 综合补丁脚本
补丁项:
1. ToolSearch 域名限制解除 — 允许 ToolSearch 访问任意域名
2. Chrome 订阅检查绕过 — /chrome 命令不再要求 claude.ai 订阅
3. Context Warning 禁用 — 禁用 context window 接近上限时的警告(不影响 auto-compact)
4. Auth conflict 警告抑制 — 因 Patch 2 导致的误触发 OBK 警告
5. Read/Search 折叠禁用 — 禁止 Read/Search 工具结果折叠
支持:
macOS / Windows / Linux
bun 官方二进制 / npm / pnpm / Homebrew / VS Code·Cursor 扩展
用法:
python patch-claude.py # 交互式
python patch-claude.py --auto # 自动补丁所有
python patch-claude.py --restore # 从备份恢复
python patch-claude.py --status # 仅查看状态
"""
import sys
import os
import re
import shutil
import platform
import subprocess
from pathlib import Path
from dataclasses import dataclass, field
from typing import Callable
# ── 补丁定义 ──────────────────────────────────────────────────────────
@dataclass
class PatchDef:
name: str
description: str
target_re: re.Pattern[bytes]
patched_re: re.Pattern[bytes]
build_replacement: Callable[[re.Match[bytes]], bytes]
default_enabled: bool = True
def _pad_to(prefix: bytes, suffix: bytes, length: int) -> bytes:
padding = length - len(prefix) - len(suffix)
if padding < 0:
raise ValueError(f"patch template too long for match length {length}")
return prefix + b" " * padding + suffix
# --- Patch 1: ToolSearch domain bypass ---
_TS_TARGET = re.compile(
rb'return\["api\.anthropic\.com"\]\.includes\('
rb'([A-Za-z_$][A-Za-z0-9_$]*)\)\}catch\{return!1\}'
)
_TS_PATCHED = re.compile(rb'return!0/\* *\*/\}catch\{return!0\}')
PATCH_TOOLSEARCH = PatchDef(
name="toolsearch",
description="ToolSearch 域名限制解除 (新版已支持 env ENABLE_TOOL_SEARCH 按需控制)",
target_re=_TS_TARGET,
patched_re=_TS_PATCHED,
build_replacement=lambda m: _pad_to(
b"return!0/*", b"*/}catch{return!0}", len(m.group(0))
),
default_enabled=False,
)
# --- Patch 2: Chrome subscription bypass ---
# Pattern A: function XX(){if(!YY())return!1;return ZZ(WW()?.scopes)} → return!0
# Pattern B: apiKey:XX()?null: → apiKey:!1 ?null: (keep API key for non-OAuth users)
_ID = rb'[A-Za-z_$][A-Za-z0-9_$]*'
_CH_TARGET = re.compile(
rb'function (' + _ID + rb')\(\)\{if\(!' + _ID + rb'\(\)\)return!1;return '
+ _ID + rb'\(' + _ID + rb'\(\)\?\.scopes\)\}'
rb'|'
rb'apiKey:' + _ID + rb'\(\)\?null:'
)
_CH_PATCHED = re.compile(
rb'function ' + _ID + rb'\(\)\{return!0 +\}'
rb'|'
rb'apiKey:!1 +\?null:'
)
def _ch_replace(m: re.Match[bytes]) -> bytes:
original = m.group(0)
if original.startswith(b'function '):
fn_name = m.group(1)
return _pad_to(b"function " + fn_name + b"(){return!0", b"}", len(original))
# apiKey:ID()?null: → apiKey:!1 ?null:
prefix = b'apiKey:'
suffix = b'?null:'
call = original[len(prefix):-len(suffix)]
padded = b'!1' + b' ' * (len(call) - 2)
return prefix + padded + suffix
PATCH_CHROME = PatchDef(
name="chrome",
description="Chrome 订阅检查绕过 (/chrome)",
target_re=_CH_TARGET,
patched_re=_CH_PATCHED,
build_replacement=_ch_replace,
)
# --- Patch 3: Context warning disable ---
# isAboveWarningThreshold:X,...}=Y,Z=FN();if(!X||Z)return null
_CW_TARGET = re.compile(
rb'isAboveWarningThreshold:(' + _ID + rb'),isAboveErrorThreshold:' + _ID
+ rb'\}=' + _ID + rb',(' + _ID + rb')=' + _ID + rb'\(\);if\(!\1\|\|\2\)return null'
)
_CW_PATCHED = re.compile(
rb'isAboveWarningThreshold:' + _ID + rb',isAboveErrorThreshold:' + _ID
+ rb'\}=' + _ID + rb',' + _ID + rb'=' + _ID + rb'\(\);if\(!0\|\|' + _ID + rb'\)return null'
)
def _cw_replace(m: re.Match[bytes]) -> bytes:
warn_var = m.group(1)
dismiss_var = m.group(2)
return m.group(0).replace(
b"if(!" + warn_var + b"||" + dismiss_var + b")",
b"if(!0||" + dismiss_var + b")",
1,
)
PATCH_CONTEXT_WARNING = PatchDef(
name="context_warning",
description="Context Warning 禁用",
target_re=_CW_TARGET,
patched_re=_CW_PATCHED,
build_replacement=_cw_replace,
)
# --- Patch 4: Auth conflict warning suppress ---
# r8() 被 patch 为 return!0 后,OBK 的 isActive 会误触发 auth conflict 警告
_AW_TARGET = re.compile(
rb'isActive:\(\)=>\{let (' + _ID + rb')=' + _ID + rb'\(\);return '
+ _ID + rb'\(\)&&\(\1\.source==="ANTHROPIC_AUTH_TOKEN"\|\|\1\.source==="apiKeyHelper"\)\}'
)
_AW_PATCHED = re.compile(
rb'isActive:\(\)=>\{return!1 +\}'
)
PATCH_AUTH_WARNING = PatchDef(
name="auth_warning",
description="Auth conflict 警告抑制",
target_re=_AW_TARGET,
patched_re=_AW_PATCHED,
build_replacement=lambda m: _pad_to(
b"isActive:()=>{return!1", b"}", len(m.group(0))
),
)
# --- Patch 5: Collapse Read/Search disable ---
# <=2.1.87: isCollapsible:XX.isSearch||XX.isRead||!1
# >=2.1.88: isCollapsible:T||(B9()?H===Aq:!1)
_CRS_TARGET = re.compile(
rb'isCollapsible:(' + _ID + rb')\.isSearch\|\|\1\.isRead\|\|!1'
rb'|'
rb'isCollapsible:' + _ID + rb'\|\|\(' + _ID + rb'\(\)\?' + _ID + rb'===' + _ID + rb':!1\)'
)
_CRS_PATCHED = re.compile(
rb'isCollapsible:!1/\* *\*/'
)
PATCH_COLLAPSE_RS = PatchDef(
name="collapse_read_search",
description="Read/Search 折叠禁用",
target_re=_CRS_TARGET,
patched_re=_CRS_PATCHED,
build_replacement=lambda m: _pad_to(
b"isCollapsible:!1/*", b"*/", len(m.group(0))
),
)
ALL_PATCHES = [PATCH_TOOLSEARCH, PATCH_CHROME, PATCH_CONTEXT_WARNING, PATCH_AUTH_WARNING, PATCH_COLLAPSE_RS]
BACKUP_SUFFIX = ".claude-patch-bak"
# ── 补丁引擎 ──────────────────────────────────────────────────────────
def get_patch_status(data: bytes, patch: PatchDef) -> str:
"""Return 'unpatched' / 'patched' / 'unknown' for a single patch."""
if patch.target_re.search(data):
return "unpatched"
if patch.patched_re.search(data):
return "patched"
return "unknown"
def apply_single_patch(data: bytes, patch: PatchDef) -> tuple[bytes, int]:
count = 0
def replacer(m: re.Match[bytes]) -> bytes:
nonlocal count
replacement = patch.build_replacement(m)
if len(replacement) != len(m.group(0)):
raise ValueError(
f"[{patch.name}] replacement length mismatch: "
f"{len(replacement)} != {len(m.group(0))}"
)
count += 1
return replacement
return patch.target_re.sub(replacer, data), count
def get_all_statuses(
data: bytes, patches: list[PatchDef] | None = None
) -> dict[str, str]:
targets = patches if patches is not None else ALL_PATCHES
return {p.name: get_patch_status(data, p) for p in targets}
def apply_all_patches(
data: bytes, patches: list[PatchDef] | None = None
) -> tuple[bytes, dict[str, int]]:
targets = patches if patches is not None else ALL_PATCHES
results: dict[str, int] = {}
for patch in targets:
data, count = apply_single_patch(data, patch)
results[patch.name] = count
return data, results
# ── 平台 & 工具 ──────────────────────────────────────────────────────
SYSTEM = platform.system()
IS_WINDOWS = (
SYSTEM == "Windows"
or "MSYS" in os.environ.get("MSYSTEM", "")
or "MINGW" in platform.platform()
)
def home() -> Path:
return Path.home()
def run_cmd(cmd: list[str], fallback: str = "") -> str:
if not shutil.which(cmd[0]):
return fallback
try:
r = subprocess.run(
cmd, capture_output=True, text=True, timeout=5, shell=IS_WINDOWS
)
return r.stdout.strip() if r.returncode == 0 else fallback
except Exception:
return fallback
def resolve_patch_target(path: Path) -> Path:
try:
return path.resolve(strict=True)
except OSError:
return path
def resign_if_needed(path: Path) -> tuple[bool, str]:
if SYSTEM != "Darwin":
return True, ""
result = subprocess.run(
["codesign", "--force", "--sign", "-", str(path)],
capture_output=True,
text=True,
)
if result.returncode == 0:
return True, "已完成 macOS ad-hoc 重签名。"
message = (result.stderr or result.stdout).strip() or "codesign 执行失败"
return False, message
# ── 安装探测 ──────────────────────────────────────────────────────────
class Installation:
def __init__(self, kind: str, target: Path, description: str):
self.kind = kind
self.source = target
self.target = resolve_patch_target(target)
self.description = description
self.backup = self.target.parent / (self.target.name + BACKUP_SUFFIX)
def __repr__(self):
return f"[{self.kind}] {self.description}\n 文件: {self.target}"
def _find_patch_target_in_pkg(pkg_dir: Path) -> Path | None:
cli_js = pkg_dir / "cli.js"
if cli_js.is_file() and b"api.anthropic.com" in cli_js.read_bytes():
return cli_js
for js_file in sorted(pkg_dir.rglob("*.js")):
if js_file.stat().st_size < 1000:
continue
try:
if b"api.anthropic.com" in js_file.read_bytes():
return js_file
except OSError:
continue
return None
def find_bun_installations() -> list[Installation]:
results = []
candidates: list[Path] = []
if IS_WINDOWS:
candidates.append(home() / ".local" / "bin" / "claude.exe")
else:
candidates.append(home() / ".claude" / "local" / "claude")
candidates.append(home() / ".local" / "bin" / "claude")
for p in candidates:
if p.is_file():
results.append(Installation("bun", p, f"Bun 官方安装 ({p})"))
return results
def find_npm_installations() -> list[Installation]:
results = []
npm_root = run_cmd(["npm", "root", "-g"])
if npm_root:
pkg_dir = Path(npm_root) / "@anthropic-ai" / "claude-code"
if pkg_dir.is_dir():
target = _find_patch_target_in_pkg(pkg_dir)
if target:
results.append(
Installation("npm", target, f"npm 全局安装 ({target})")
)
else:
results.extend(_find_npm_fallback())
return results
def _find_npm_fallback() -> list[Installation]:
h = home()
search_dirs: list[tuple[Path, str]] = []
if IS_WINDOWS:
appdata = Path(os.environ.get("APPDATA", ""))
if appdata.name:
search_dirs.append((appdata / "npm" / "node_modules", "npm 默认全局"))
nvm_home = os.environ.get(
"NVM_HOME", str(appdata / "nvm") if appdata.name else ""
)
if nvm_home:
nvm_path = Path(nvm_home)
if nvm_path.is_dir():
for d in nvm_path.iterdir():
nm = d / "node_modules"
if d.is_dir() and nm.is_dir():
search_dirs.append((nm, f"nvm ({d.name})"))
fnm_dir = Path(os.environ.get("FNM_DIR", str(h / ".fnm")))
nv = fnm_dir / "node-versions"
if nv.is_dir():
for d in nv.iterdir():
nm = d / "installation" / "node_modules"
if nm.is_dir():
search_dirs.append((nm, f"fnm ({d.name})"))
else:
nvm_dir = Path(os.environ.get("NVM_DIR", str(h / ".nvm")))
versions = nvm_dir / "versions" / "node"
if versions.is_dir():
for d in versions.iterdir():
nm = d / "lib" / "node_modules"
if nm.is_dir():
search_dirs.append((nm, f"nvm ({d.name})"))
fnm_dir = Path(os.environ.get("FNM_DIR", str(h / ".fnm")))
nv = fnm_dir / "node-versions"
if nv.is_dir():
for d in nv.iterdir():
nm = d / "installation" / "lib" / "node_modules"
if nm.is_dir():
search_dirs.append((nm, f"fnm ({d.name})"))
for p in [
Path("/usr/local/lib/node_modules"),
Path("/usr/lib/node_modules"),
]:
if p.is_dir():
search_dirs.append((p, "系统 npm"))
volta_home = Path(os.environ.get("VOLTA_HOME", str(h / ".volta")))
volta_node = volta_home / "tools" / "image" / "node"
if volta_node.is_dir():
for d in volta_node.iterdir():
nm = (d / "node_modules") if IS_WINDOWS else (d / "lib" / "node_modules")
if nm.is_dir():
search_dirs.append((nm, f"volta ({d.name})"))
results: list[Installation] = []
seen: set[str] = set()
for nm_dir, desc in search_dirs:
pkg_dir = nm_dir / "@anthropic-ai" / "claude-code"
if pkg_dir.is_dir():
target = _find_patch_target_in_pkg(pkg_dir)
if target:
key = str(target.resolve())
if key not in seen:
seen.add(key)
results.append(
Installation("npm", target, f"npm ({desc}) ({target})")
)
return results
def find_pnpm_installations() -> list[Installation]:
results = []
pnpm_root = run_cmd(["pnpm", "root", "-g"])
if not pnpm_root:
return results
pkg_dir = Path(pnpm_root) / "@anthropic-ai" / "claude-code"
if pkg_dir.is_dir():
target = _find_patch_target_in_pkg(pkg_dir)
if target:
results.append(
Installation(
"pnpm", target.resolve(), f"pnpm 全局安装 ({target.resolve()})"
)
)
return results
pnpm_dir = Path(pnpm_root).parent / ".pnpm"
if pnpm_dir.is_dir():
for pkg in pnpm_dir.rglob("@anthropic-ai/claude-code"):
if pkg.is_dir():
target = _find_patch_target_in_pkg(pkg)
if target:
results.append(
Installation("pnpm", target, f"pnpm 全局安装 ({target})")
)
break
return results
def find_brew_installations() -> list[Installation]:
if IS_WINDOWS:
return []
results = []
seen: set[str] = set()
brew_prefix = run_cmd(["brew", "--prefix"])
prefixes: list[Path] = []
if brew_prefix:
prefixes.append(Path(brew_prefix))
for fallback in [Path("/opt/homebrew"), Path("/usr/local")]:
if fallback not in prefixes and fallback.is_dir():
prefixes.append(fallback)
for prefix in prefixes:
caskroom = prefix / "Caskroom" / "claude-code"
if not caskroom.is_dir():
continue
for version_dir in sorted(caskroom.iterdir(), reverse=True):
if not version_dir.is_dir() or version_dir.name.startswith("."):
continue
binary = version_dir / "claude"
if binary.is_file() and binary.stat().st_size > 10 * 1024 * 1024:
key = str(binary.resolve())
if key not in seen:
seen.add(key)
results.append(
Installation(
"brew", binary, f"Homebrew cask ({version_dir.name})"
)
)
return results
def find_vscode_installations() -> list[Installation]:
results = []
search_bases = [
("vscode", "VS Code", home() / ".vscode" / "extensions"),
("vscode", "VS Code Insiders", home() / ".vscode-insiders" / "extensions"),
("cursor", "Cursor", home() / ".cursor" / "extensions"),
]
for kind, label, base in search_bases:
if not base.is_dir():
continue
ext_dirs = sorted(base.glob("anthropic.claude-code-*"), reverse=True)
if not ext_dirs:
continue
ext_dir = ext_dirs[0]
for name in ["claude.exe", "claude"] if IS_WINDOWS else ["claude"]:
for p in ext_dir.rglob(name):
if (
p.is_file()
and not p.name.endswith(".bak")
and p.stat().st_size > 10 * 1024 * 1024
):
results.append(
Installation(
kind, p, f"{label} 捆绑二进制 ({ext_dir.name})"
)
)
return results
def find_wsl_installations() -> list[Installation]:
if not IS_WINDOWS:
return []
distro_names = _get_wsl_distros()
if not distro_names:
return []
def _safe_is_dir(p: Path) -> bool:
try:
return p.is_dir()
except OSError:
return False
results: list[Installation] = []
for dname in distro_names:
distro = Path(f"//wsl.localhost/{dname}")
if not _safe_is_dir(distro):
continue
user_dirs: list[Path] = []
home_base = distro / "home"
if _safe_is_dir(home_base):
try:
user_dirs.extend(d for d in home_base.iterdir() if d.is_dir())
except OSError:
pass
root_home = distro / "root"
if _safe_is_dir(root_home):
user_dirs.append(root_home)
for udir in user_dirs:
versions_dir = udir / ".local" / "share" / "claude" / "versions"
if _safe_is_dir(versions_dir):
try:
for v in sorted(versions_dir.iterdir(), reverse=True):
if v.name.endswith(BACKUP_SUFFIX):
continue
if v.is_file() and v.stat().st_size > 10 * 1024 * 1024:
results.append(
Installation(
"wsl", v, f"WSL ({dname}) bun v{v.name}"
)
)
except OSError:
pass
nm_search: list[tuple[Path, str]] = []
nvm_node = udir / ".nvm" / "versions" / "node"
if _safe_is_dir(nvm_node):
try:
for v in nvm_node.iterdir():
nm = v / "lib" / "node_modules"
if v.is_dir() and nm.is_dir():
nm_search.append((nm, f"nvm {v.name}"))
except OSError:
pass
fnm_nv = udir / ".fnm" / "node-versions"
if _safe_is_dir(fnm_nv):
try:
for v in fnm_nv.iterdir():
nm = v / "installation" / "lib" / "node_modules"
if v.is_dir() and nm.is_dir():
nm_search.append((nm, f"fnm {v.name}"))
except OSError:
pass
volta_node = udir / ".volta" / "tools" / "image" / "node"
if _safe_is_dir(volta_node):
try:
for v in volta_node.iterdir():
nm = v / "lib" / "node_modules"
if v.is_dir() and nm.is_dir():
nm_search.append((nm, f"volta {v.name}"))
except OSError:
pass
for nm_dir, label in nm_search:
pkg = nm_dir / "@anthropic-ai" / "claude-code"
if _safe_is_dir(pkg):
target = _find_patch_target_in_pkg(pkg)
if target:
results.append(
Installation("wsl", target, f"WSL ({dname}) {label}")
)
for sys_nm in [
distro / "usr" / "local" / "lib" / "node_modules",
distro / "usr" / "lib" / "node_modules",
]:
pkg = sys_nm / "@anthropic-ai" / "claude-code"
if _safe_is_dir(pkg):
target = _find_patch_target_in_pkg(pkg)
if target:
results.append(
Installation("wsl", target, f"WSL ({dname}) npm 系统")
)
return results
def _get_wsl_distros() -> list[str]:
try:
r = subprocess.run(
["wsl.exe", "--list", "--quiet"], capture_output=True, timeout=5
)
if r.returncode != 0:
return []
text = r.stdout.decode("utf-16-le", errors="ignore").strip()
return [line.strip() for line in text.splitlines() if line.strip()]
except Exception:
return []
def find_all_installations() -> list[Installation]:
all_inst: list[Installation] = []
all_inst.extend(find_brew_installations())
all_inst.extend(find_bun_installations())
all_inst.extend(find_npm_installations())
all_inst.extend(find_pnpm_installations())
all_inst.extend(find_vscode_installations())
all_inst.extend(find_wsl_installations())
seen: set[str] = set()
deduped: list[Installation] = []
for inst in all_inst:
key = str(inst.target)
if key not in seen:
seen.add(key)
deduped.append(inst)
return deduped
# ── 补丁操作 ──────────────────────────────────────────────────────────
STATUS_SYMBOL = {"unpatched": "○", "patched": "●", "unknown": "?"}
STATUS_LABEL = {"unpatched": "未补丁", "patched": "已补丁", "unknown": "不兼容"}
def _write_via_rename(target: Path, data: bytes) -> bool:
tmp_path = target.with_suffix(target.suffix + ".tmp")
old_path = target.with_suffix(target.suffix + ".old")
for p in (tmp_path, old_path):
try:
p.unlink(missing_ok=True)
except OSError:
pass
tmp_path.write_bytes(data)
try:
target.rename(old_path)
except OSError:
tmp_path.unlink(missing_ok=True)
print(f" ✗ 无法重命名 {target.name},请关闭 claude 后重试。")
return False
tmp_path.rename(target)
try:
old_path.unlink(missing_ok=True)
except OSError:
pass
return True
def apply_patch(
inst: Installation, patches: list[PatchDef] | None = None
) -> bool:
targets = patches if patches is not None else ALL_PATCHES
data = inst.target.read_bytes()
statuses = get_all_statuses(data, targets)
has_unpatched = any(s == "unpatched" for s in statuses.values())
all_patched = all(s == "patched" for s in statuses.values())
if all_patched:
print(" ✓ 所有补丁已应用,跳过。")
return True
if not has_unpatched:
unknown = [
p.description
for p in targets
if statuses[p.name] == "unknown"
]
if unknown:
print(f" ✗ 以下补丁目标未找到(版本不兼容):")
for desc in unknown:
print(f" - {desc}")
return False
patched_data, counts = apply_all_patches(data, targets)
total = sum(counts.values())
if total == 0:
print(" ✗ 未找到任何可补丁的目标。")
return False
# backup
if not inst.backup.is_file():
shutil.copy2(inst.target, inst.backup)
print(f" 已备份到 {inst.backup}")
try:
inst.target.write_bytes(patched_data)
except PermissionError:
print(" 文件被占用,使用重命名方式替换...")
if not _write_via_rename(inst.target, patched_data):
return False
ok, message = resign_if_needed(inst.target)
if not ok:
print(f" ✗ 补丁已写入,但重签名失败: {message}")
return False
if message:
print(f" {message}")
for patch in targets:
c = counts[patch.name]
status = statuses[patch.name]
if c > 0:
print(f" ✓ {patch.description}: 替换 {c} 处")
elif status == "patched":
print(f" - {patch.description}: 已是最新")
else:
print(f" ✗ {patch.description}: 未找到目标")
return True
def restore_backup(inst: Installation) -> bool:
if not inst.backup.is_file():
print(f" ✗ 未找到备份文件 {inst.backup}")
return False
backup_data = inst.backup.read_bytes()
try:
inst.target.write_bytes(backup_data)
except PermissionError:
print(" 文件被占用,使用重命名方式替换...")
if not _write_via_rename(inst.target, backup_data):
return False
ok, message = resign_if_needed(inst.target)
if not ok:
print(f" ✗ 已恢复,但重签名失败: {message}")
return False
if message:
print(f" {message}")
print(" ✓ 已从备份恢复。")
return True
def print_status(
inst: Installation, patches: list[PatchDef] | None = None
):
targets = patches if patches is not None else ALL_PATCHES
data = inst.target.read_bytes()
statuses = get_all_statuses(data, targets)
has_backup = "有备份" if inst.backup.is_file() else "无备份"
print(f" {inst} | {has_backup}")
for patch in targets:
s = statuses[patch.name]
print(f" {STATUS_SYMBOL[s]} {patch.description}: {STATUS_LABEL[s]}")
print()
# ── 交互式 TUI ───────────────────────────────────────────────────────
# ANSI colors
_C_RESET = "\033[0m"
_C_BOLD = "\033[1m"
_C_GREEN = "\033[32m"
_C_YELLOW = "\033[33m"
_C_RED = "\033[31m"
_C_CYAN = "\033[36m"
_C_DIM = "\033[2m"
STATUS_COLOR = {"unpatched": _C_YELLOW, "patched": _C_GREEN, "unknown": _C_RED}
_PATCH_LETTERS = "abcdefghijklmnopqrstuvwxyz"
def _clear_screen():
print("\033[2J\033[H", end="", flush=True)
def _read_char() -> str:
"""Read a single character without waiting for Enter (raw mode)."""
if IS_WINDOWS:
import msvcrt
ch = msvcrt.getwch()
return ch
import tty
import termios
fd = sys.stdin.fileno()
old = termios.tcgetattr(fd)
try:
tty.setraw(fd)
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old)
return ch
def _draw_tui(
installations: list[Installation],
enabled: list[bool],
mode: str,
inst_data: list[bytes],
):
action = "补丁" if mode == "patch" else "恢复"
max_letter = _PATCH_LETTERS[len(ALL_PATCHES) - 1]
print(f"{_C_BOLD}{'=' * 60}")
print(" Claude Code 综合补丁工具")
print(f"{'=' * 60}{_C_RESET}")
print(f" 系统: {SYSTEM} | Python {platform.python_version()}")
print()
# patch toggles
print(f"补丁项 ({_C_CYAN}a-{max_letter} 切换{_C_RESET}):")
for i, patch in enumerate(ALL_PATCHES):
letter = _PATCH_LETTERS[i]
if enabled[i]:
mark = f"{_C_GREEN}x{_C_RESET}"
else:
mark = " "
# dim styling for non-default disabled patches
if not patch.default_enabled and not enabled[i]:
desc = f"{_C_DIM}{patch.description}{_C_RESET}"
else:
desc = patch.description
print(f" {letter}. [{mark}] {desc}")
print()
# legend
print(
f" {_C_GREEN}●{_C_RESET} 已补丁 "
f"{_C_YELLOW}○{_C_RESET} 未补丁 "
f"{_C_RED}?{_C_RESET} 不兼容 "
f"{_C_DIM}|{_C_RESET} "
f"{_C_YELLOW}黄色{_C_RESET}=将处理 "
f"{_C_GREEN}绿色{_C_RESET}=已处理 "
f"{_C_DIM}灰色=跳过{_C_RESET}"
)
print()
# installations — always check ALL patches
n = len(installations)
print(f"检测到 {n} 个安装:")
print()
for i, inst in enumerate(installations, 1):
data = inst_data[i - 1]
has_backup = "有备份" if inst.backup.is_file() else "无备份"
indicators: list[str] = []
for j, patch in enumerate(ALL_PATCHES):
letter = _PATCH_LETTERS[j]
s = get_patch_status(data, patch)
symbol = STATUS_SYMBOL[s]
# each cell: "S L" padded to 5 visible chars ("● a ")
visible_text = f"{symbol} {letter}"
padding = " " * (5 - len(visible_text))
if enabled[j]:
color = STATUS_COLOR[s]
indicators.append(f"{color}{visible_text}{_C_RESET}{padding}")
else:
indicators.append(f"{_C_DIM}{visible_text}{_C_RESET}{padding}")
status_line = "".join(indicators)
print(f" {i}. [{inst.kind:6s}] {status_line}| {has_backup}")
print(f" {_C_DIM}{inst.target}{_C_RESET}")
print()
if n > 1:
print(f" 0. 全部{action}")
print()
# prompt
num_range = f"1-{n}" if n > 1 else "1"
zero_hint = " | 0 全部" if n > 1 else ""
print(
f"{_C_CYAN}按 a-{max_letter} 切换补丁项 | "
f"{num_range} {action}指定目标{zero_hint} | q 退出{_C_RESET}",
flush=True,
)
def interactive_tui(installations: list[Installation], mode: str = "patch"):
enabled = [p.default_enabled for p in ALL_PATCHES]
inst_data = [inst.target.read_bytes() for inst in installations]
_clear_screen()
_draw_tui(installations, enabled, mode, inst_data)
while True:
try:
ch = _read_char()
except (EOFError, KeyboardInterrupt):
print("\n已取消。")
return
choice = ch.lower()
if choice in ("\x03", "\x1b"): # Ctrl-C / Esc
print("\n已取消。")
return
if choice == "q":
print("\n已取消。")
return
# toggle patch: a-z
if choice in _PATCH_LETTERS:
idx = _PATCH_LETTERS.index(choice)
if idx < len(ALL_PATCHES):
enabled[idx] = not enabled[idx]
_clear_screen()
_draw_tui(installations, enabled, mode, inst_data)
continue
# select installation: number
if choice.isdigit():
num = int(choice)
n = len(installations)
if num == 0 and n > 1:
target_indices = list(range(n))
elif 1 <= num <= n:
target_indices = [num - 1]
else:
continue
selected = [p for p, e in zip(ALL_PATCHES, enabled) if e]
if not selected:
print(f"\n{_C_RED}未选择任何补丁项。{_C_RESET}")
continue
for idx in target_indices:
inst = installations[idx]
print(f"\n→ 处理: {inst}")
if mode == "patch":
apply_patch(inst, selected)
else:
restore_backup(inst)
print(f"\n{_C_GREEN}完成。重启 claude 生效。{_C_RESET}")
return
def auto_mode(
installations: list[Installation],
mode: str = "patch",
patches: list[PatchDef] | None = None,
):
if patches is None:
# --auto mode: only apply default_enabled patches
patches = [p for p in ALL_PATCHES if p.default_enabled]
for inst in installations:
print(f"\n→ 处理: {inst}")
if mode == "patch":
apply_patch(inst, patches)
else:
restore_backup(inst)
print("\n完成。重启 claude 生效。")
# ── 入口 ──────────────────────────────────────────────────────────────
def main():
mode = "patch"
auto = False
status_only = False
for arg in sys.argv[1:]:
if arg == "--restore":
mode = "restore"
elif arg == "--auto":
auto = True
elif arg == "--status":
status_only = True
is_interactive = not auto and not status_only
def _print_header():
print(f"{_C_BOLD}{'=' * 60}")
print(" Claude Code 综合补丁工具")
print(f"{'=' * 60}{_C_RESET}")
print(f" 系统: {SYSTEM} | Python {platform.python_version()}")
print()
# interactive mode: show scanning on a clean screen, TUI will redraw
if is_interactive:
_clear_screen()
_print_header()
print("正在扫描 Claude Code 安装...", flush=True)
else:
_print_header()
print("正在扫描 Claude Code 安装...")
installations = find_all_installations()
if not installations:
print(f"\n{_C_RED}未检测到任何 Claude Code 安装。{_C_RESET}")
print("支持: bun / npm -g / pnpm / Homebrew / VS Code·Cursor")
sys.exit(1)
if status_only:
print(f"\n检测到 {len(installations)} 个安装:\n")
for inst in installations:
print_status(inst)
_pause_if_needed(auto)
return
if auto:
auto_mode(installations, mode)
else:
interactive_tui(installations, mode)
_pause_if_needed(auto)
def _pause_if_needed(auto: bool):
if auto:
return
try:
input("\n按回车键退出...")
except (EOFError, KeyboardInterrupt):
pass
def _tui_demo():
"""Render _draw_tui with mock data for visual verification."""
from pathlib import Path
import tempfile, re
# create mock PatchDef objects
class MockPatch:
def __init__(self, name, description, default_enabled=True):
self.name = name
self.description = description
self.default_enabled = default_enabled
self.target_re = re.compile(rb"NEVER_MATCH_TARGET")
self.patched_re = re.compile(rb"NEVER_MATCH_PATCHED")
self.build_replacement = lambda m: m.group(0)
# override ALL_PATCHES temporarily
global ALL_PATCHES
saved = ALL_PATCHES
ALL_PATCHES = [
MockPatch("toolsearch", "ToolSearch 全域", default_enabled=False),
MockPatch("chrome", "Chrome 订阅绕过"),
MockPatch("context_warning", "Context Warning 禁用"),
MockPatch("auth_warning", "Auth conflict 警告抑制"),
MockPatch("collapse_rs", "Read/Search 折叠禁用"),
]
# create mock installations with temp files
tmp = Path(tempfile.mkdtemp())
mock_files = []
for name in ["cli.js", "bundle.js"]:
p = tmp / name
p.write_bytes(b"dummy")
mock_files.append(p)
class MockInstallation:
def __init__(self, kind, target, description):
self.kind = kind
self.target = target
self.description = description
self.backup = target.parent / (target.name + ".bak")
insts = [
MockInstallation("bun", mock_files[0], "Bun 官方安装"),
MockInstallation("vscode", mock_files[1], "VS Code 扩展"),
]
# mock data: all "unknown" since patterns won't match
inst_data = [f.read_bytes() for f in mock_files]
# enabled: first patch (toolsearch) disabled, rest enabled
enabled = [False, True, True, True, True]
print("\n=== TUI Demo (all statuses = unknown) ===\n")
_draw_tui(insts, enabled, "patch", inst_data)
# now test with patched_re matching to simulate "patched" status
ALL_PATCHES[1].patched_re = re.compile(rb"dummy") # chrome -> patched
ALL_PATCHES[2].target_re = re.compile(rb"dummy") # context_warning -> unpatched
# auth_warning stays unknown, collapse_rs stays unknown
inst_data = [f.read_bytes() for f in mock_files]
print("\n=== TUI Demo (mixed statuses) ===\n")
_draw_tui(insts, enabled, "patch", inst_data)
# cleanup
ALL_PATCHES = saved
for f in mock_files:
f.unlink()
tmp.rmdir()
if __name__ == "__main__":
import sys as _sys
if "--tui-demo" in _sys.argv:
_tui_demo()
else:
main()
保存为 patch-claude.py 后 python3 patch-claude.py 执行
image972×678 44.5 KB
image2348×1714 275 KB
试了下没啥问题,可以正常使用,感谢两位佬的付出
感谢楼里佬提的优化建议
优化了一下 TUI,支持选择补丁项目,默认不选中 ToolSearch
然后把哈雷佬的 关于 2.1.20 上线的 collapsed read/search groups
也集成进去了,代码已经更新到上面的 python 代码块中
image1910×1358 323 KB
网友解答:--【壹】--:
mark一下,明天看看,感谢大佬分享
--【贰】--: 哈雷彗星:
不过有1M了感觉开这个有点脱裤子放屁 我花十几k还吃不下这坨基础token么 离20%都好远(ノ_ _)ノ
这是幸福的烦恼
我用量不多用的 Pro 然后用 sub2api 保活,所以正好没有 1M 上下文并且是通过 API 调用,还是比较刚需的
不过现在能用 env 控制了确实没有必要特地 patch Tool Search 了
--【叁】--:
mark了,一会儿试试
--【肆】--:
先mark下, 免得明天找不到了
--【伍】--:
明天试试
--【陆】--:
mark 一下,明天试试
--【柒】--:
mark.稍后再看
--【捌】--:
原来如此
--【玖】--:
这么强!
--【拾】--:
就两个版本的事吧 就落盘了 灰度特性就是这样的
没放出来是没想清楚怎么调整
后来的逻辑是 非 api.anthropic.com 默认是false 因为本就不能保证第三方能够支持这个
所以只要用三方的话 只需要 加ENV控制打开即可(想开的情况下)
不过有1M了感觉开这个有点脱裤子放屁 我花十几k还吃不下这坨基础token么 离20%都好远(ノ_ _)ノ
--【拾壹】--:
好快的进场,刚要预言你的到来
其实略有神秘
--【拾贰】--:
不标记了,直接运行
image503×162 4.29 KB
--【拾叁】--:
是得下载好 CCometixLine执行这个脚本才有意义吗?
--【拾肆】--:
是因为 1M 上下文还是因为 cc 官方修了
--【拾伍】--:
看了眼,好恐怖一哈雷
--【拾陆】--:
明天试试看
--【拾柒】--:
哦,好像是 ENABLE_TOOL_SEARCH 这个 env 能直接控制了
--【拾捌】--:
先mark下, 免得明天找不到了
--【拾玖】--:
不是 ToolSearch已经不再需要补丁了w
还有就是 VCC的话 直接把native-binary 文件夹删了 再放置 claude-code 的npm包就是一样用了 bun版狗都不用

