参考 Claude Code强制开启 Tool Search工具分享 的分享,做成了一个 py 交互脚本,支持多种功能,也包括 fetch 域名限制关闭,格式好像有点小问题,拿 ai 修复吧
#!/usr/bin/env python3
“”"
Claude Code Feature Gate 综合补丁工具用法:
python patch-claude-features.py # 交互式菜单
python patch-claude-features.py --auto # 全部开启
python patch-claude-features.py --restore # 从备份恢复
“”"import sys
import os
import re
import shutil
import platform
import subprocess
from pathlib import Path── 补丁目标定义 ─────────────────────────────────────────────────────
每个条目: (gate 名, 显示名, 分类, 排除后缀)
FEATURE_GATES: list[tuple[str, str, str, list[str]]] = [
# 用户功能
(“tengu_kairos_cron”, “/loop 定时循环任务”, “用户功能”, [“_config”]),
(“tengu_session_memory”, “会话级记忆”, “用户功能”, ),
(“tengu_auto_background_agents”, “自动后台 Agent”, “用户功能”, ),
(“tengu_keybinding_customization_release”,“快捷键自定义”, “用户功能”, ),
(“tengu_pr_status_cli”, “PR 状态显示”, “用户功能”, ),
(“tengu_mcp_elicitation”, “MCP 交互式询问”, “用户功能”, ),
(“tengu_immediate_model_command”, “即时模型切换”, “用户功能”, ),
(“tengu_chrome_auto_enable”, “Chrome MCP 自动集成”, “用户功能”, ),
(“tengu_plan_mode_interview_phase”, “Plan 模式访谈阶段”, “用户功能”, ),
(“tengu_amber_quartz”, “语音功能”, “用户功能”, ),
# 安全与稳定
(“tengu_destructive_command_warning”, “危险命令警告”, “安全稳定”, ),
(“tengu_permission_explainer”, “权限风险解释”, “安全稳定”, ),
(“tengu_compact_streaming_retry”, “compact 流式重试”, “安全稳定”, ),
(“tengu_compact_cache_prefix”, “compact 缓存前缀优化”, “安全稳定”, ),
(“tengu_summarize_tool_results”, “工具结果摘要”, “安全稳定”, ),
]ToolSearch 域名白名单补丁(独立于 feature gate)
TOOLSEARCH_TARGET_RE = re.compile(
rb’return[“api.anthropic.com”].includes([A-Za-z_$][A-Za-z0-9_$]*)}catch{return!1}’
)
TOOLSEARCH_PATCHED_RE = re.compile(rb’return!0/* */}catch{return!0}')
_TS_PREFIX = b’return!0/’
_TS_SUFFIX = b’*/}catch{return!0}’TOOLSEARCH_LABEL = “ToolSearch 域名白名单解除”
def _build_gate_regex(gate_name: str, excludes: list[str]) → re.Pattern[bytes]:
“”“构建匹配 gate 默认值为 !1 的正则”“”
escaped = re.escape(gate_name.encode())
if excludes:
lookahead = b"“.join(
b”(?!" + re.escape(s.encode()) + b")" for s in excludes
)
return re.compile(escaped + b’“,!1’ + lookahead)
return re.compile(escaped + b’”,!1’)def _build_gate_patched_regex(gate_name: str, excludes: list[str]) → re.Pattern[bytes]:
“”“构建匹配已补丁状态的正则”“”
escaped = re.escape(gate_name.encode())
if excludes:
lookahead = b"“.join(
b”(?!" + re.escape(s.encode()) + b")" for s in excludes
)
return re.compile(escaped + b’“,!0’ + lookahead)
return re.compile(escaped + b’”,!0’)── 补丁项抽象 ───────────────────────────────────────────────────────
class PatchItem:
“”“单个可补丁项”“”def __init__(self, item_id: str, label: str, category: str, target_re: re.Pattern[bytes], patched_re: re.Pattern[bytes], replace_fn, revert_fn=None): self.item_id = item_id self.label = label self.category = category self.target_re = target_re self.patched_re = patched_re self.replace_fn = replace_fn self.revert_fn = revert_fn def status(self, data: bytes) -> str: has_target = bool(self.target_re.search(data)) has_patched = bool(self.patched_re.search(data)) if has_target and not has_patched: return "off" if has_patched and not has_target: return "on" if has_target and has_patched: return "partial" return "unknown" def apply(self, data: bytes) -> tuple[bytes, int]: count = 0 def _replace(m: re.Match[bytes]) -> bytes: nonlocal count count += 1 return self.replace_fn(m) return self.target_re.sub(_replace, data), count def revert(self, data: bytes) -> tuple[bytes, int]: if not self.revert_fn: return data, 0 count = 0 def _replace(m: re.Match[bytes]) -> bytes: nonlocal count count += 1 return self.revert_fn(m) return self.patched_re.sub(_replace, data), countdef _build_all_patches() → list[PatchItem]:
“”“构建全部补丁项列表”“”
items: list[PatchItem] =# Feature gates for gate_name, label, category, excludes in FEATURE_GATES: target_re = _build_gate_regex(gate_name, excludes) patched_re = _build_gate_patched_regex(gate_name, excludes) def make_replace(name_bytes=gate_name.encode()): def _fn(m: re.Match[bytes]) -> bytes: return m.group(0).replace(b"!1", b"!0", 1) return _fn def make_revert(name_bytes=gate_name.encode()): def _fn(m: re.Match[bytes]) -> bytes: return m.group(0).replace(b"!0", b"!1", 1) return _fn items.append(PatchItem( item_id=gate_name, label=label, category=category, target_re=target_re, patched_re=patched_re, replace_fn=make_replace(), revert_fn=make_revert(), )) # ToolSearch 域名白名单 def _ts_replace(m: re.Match[bytes]) -> bytes: full_len = len(m.group(0)) padding = full_len - len(_TS_PREFIX) - len(_TS_SUFFIX) if padding < 0: return m.group(0) return _TS_PREFIX + (b' ' * padding) + _TS_SUFFIX items.append(PatchItem( item_id="toolsearch_domain", label=TOOLSEARCH_LABEL, category="白名单", target_re=TOOLSEARCH_TARGET_RE, patched_re=TOOLSEARCH_PATCHED_RE, replace_fn=_ts_replace, )) # WebFetch 域名预检: blocked → allowed(等长,7字节对齐) # can_fetch===!0?{status:"allowed"}:{status:"blocked"} # ^^^^^^^ → allowed _fb_old = b'can_fetch===' + bytes([0x21]) + b'0?{status:"allowed"}:{status:"blocked"}' _fb_new = b'can_fetch===' + bytes([0x21]) + b'0?{status:"allowed"}:{status:"allowed"}' items.append(PatchItem( item_id="fetch_preflight_blocked", label="WebFetch 预检 blocked 绕过", category="白名单", target_re=re.compile(re.escape(_fb_old)), patched_re=re.compile(re.escape(_fb_new)), replace_fn=lambda m, _n=_fb_new: _n, revert_fn=lambda m, _o=_fb_old: _o, )) # WebFetch 域名预检: check_failed → allowed(等长,注释填充对齐) # {status:"check_failed",error: → {status:"allowed"/* */,error: # ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ # 14 bytes 14 bytes (allowed + 注释填充) _fc_old = b'{status:"check_failed",error:' _fc_new = b'{status:"allowed"/* */,error:' items.append(PatchItem( item_id="fetch_preflight_checkfail", label="WebFetch 预检 check_failed 绕过", category="白名单", target_re=re.compile(re.escape(_fc_old)), patched_re=re.compile(re.escape(_fc_new)), replace_fn=lambda m, _n=_fc_new: _n, revert_fn=lambda m, _o=_fc_old: _o, )) return itemsALL_PATCHES = _build_all_patches()
── 平台与安装探测 ───────────────────────────────────────────────────
SYSTEM = platform.system()
IS_WINDOWS = (
SYSTEM == “Windows”
or “MSYS” in os.environ.get(“MSYSTEM”, “”)
or “MINGW” in platform.platform()
)BACKUP_SUFFIX = “.features-bak”
def home() → Path:
return Path.home()def run_cmd(cmd: list[str], fallback: str = “”) → str:
try:
r = subprocess.run(
cmd, capture_output=True, text=True, timeout=10,
shell=IS_WINDOWS,
)
return r.stdout.strip() if r.returncode == 0 else fallback
except Exception:
return fallbackclass Installation:
def init(self, kind: str, target: Path, description: str):
self.kind = kind
self.source = target
self.target = self._resolve(target)
self.description = description
self.backup = self.target.parent / (self.target.name + BACKUP_SUFFIX)@staticmethod def _resolve(p: Path) -> Path: try: return p.resolve(strict=True) except OSError: return p def __repr__(self): return f"[{self.kind}] {self.description}\n 文件: {self.target}"def _find_bun() → 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 resultsdef _find_npm() → list[Installation]:
npm_root = run_cmd([“npm”, “root”, “-g”])
if not npm_root:
return
cli = Path(npm_root) / “@anthropic-ai” / “claude-code” / “cli.js”
if cli.is_file():
return [Installation(“npm”, cli, f"npm 全局安装 ({cli})")]
returndef _find_pnpm() → list[Installation]:
pnpm_root = run_cmd([“pnpm”, “root”, “-g”])
if not pnpm_root:
return
cli = Path(pnpm_root) / “@anthropic-ai” / “claude-code” / “cli.js”
if cli.is_file():
return [Installation(“pnpm”, cli.resolve(), f"pnpm 全局安装 ({cli.resolve()})“)]
pnpm_dir = Path(pnpm_root).parent / “.pnpm”
if pnpm_dir.is_dir():
for f in pnpm_dir.rglob(”@anthropic-ai/claude-code/cli.js"):
if f.is_file():
return [Installation(“pnpm”, f, f"pnpm 全局安装 ({f})")]
returndef find_all() → list[Installation]:
return _find_bun() + _find_npm() + _find_pnpm()── 文件写入 ─────────────────────────────────────────────────────────
def _write_via_rename(target: Path, data: bytes) → bool:
“”“Windows 下运行中的 exe 可重命名但不可覆写”“”
tmp = target.with_suffix(target.suffix + “.tmp”)
old = target.with_suffix(target.suffix + “.old”)
for p in (tmp, old):
try:
p.unlink(missing_ok=True)
except OSError:
pass
tmp.write_bytes(data)
try:
target.rename(old)
except OSError:
tmp.unlink(missing_ok=True)
print(f" x 无法重命名 {target.name},请关闭 claude 后重试。")
return False
tmp.rename(target)
try:
old.unlink(missing_ok=True)
except OSError:
pass
return Truedef write_safe(target: Path, data: bytes) → bool:
try:
target.write_bytes(data)
return True
except PermissionError:
print(" 文件被占用,使用重命名方式替换…")
return _write_via_rename(target, data)def resign_if_needed(path: Path) → tuple[bool, str]:
if SYSTEM != “Darwin”:
return True, “”
r = subprocess.run(
[“codesign”, “–force”, “–sign”, “-”, str(path)],
capture_output=True, text=True,
)
if r.returncode == 0:
return True, “已完成 macOS ad-hoc 重签名。”
msg = (r.stderr or r.stdout).strip() or “codesign 执行失败”
return False, msg── 状态显示 ─────────────────────────────────────────────────────────
def _display_width(s: str) → int:
“”“计算字符串的终端显示宽度(CJK 字符占 2 列)”“”
import unicodedata
w = 0
for ch in s:
ea = unicodedata.east_asian_width(ch)
w += 2 if ea in (“W”, “F”) else 1
return wdef _pad_right(s: str, width: int) → str:
“”“按显示宽度右填充空格”“”
return s + " " * (width - _display_width(s))STATUS_ICON = {
“off”: “”,
“on”: “[*]”,
“partial”: “[~]”,
“unknown”: “[?]”,
}STATUS_TEXT = {
“off”: “未开启”,
“on”: “已开启”,
“partial”: “部分”,
“unknown”: “不兼容”,
}def show_table(data: bytes):
“”“显示功能列表(纯状态,无 desired 标记)”“”
print()
prev_cat = “”
for i, patch in enumerate(ALL_PATCHES):
if patch.category != prev_cat:
print(f" ── {patch.category} {‘─’ * (50 - len(patch.category) * 2)}")
prev_cat = patch.categorycur = patch.status(data) icon = STATUS_ICON.get(cur, "[?]") text = STATUS_TEXT.get(cur, "???") idx = f"{i + 1:>2}" label_col = _pad_right(patch.label, 34) print(f" {idx}. {icon} {label_col} {text}") print()── 交互式菜单 ───────────────────────────────────────────────────────
def _parse_indices(cmd: str) → list[int] | None:
“”“解析编号/范围,返回 0-based 索引列表,无效返回 None”“”
indices =
for part in cmd.replace(“,”, " “).split():
try:
if “-” in part:
lo, hi = part.split(”-", 1)
lo, hi = int(lo), int(hi)
indices.extend(range(lo - 1, min(hi, len(ALL_PATCHES))))
else:
idx = int(part) - 1
if 0 <= idx < len(ALL_PATCHES):
indices.append(idx)
except ValueError:
return None
return indices if indices else Nonedef _apply_patches(inst: Installation, data: bytes,
patches: list[PatchItem], action: str) → bytes | None:
“”“执行补丁并写入文件,返回新 data 或失败返回 None”“”
if not inst.backup.is_file():
shutil.copy2(inst.target, inst.backup)
print(f" 已备份到 {inst.backup}")new_data = data total = 0 for patch in patches: if action == "open": new_data, c = patch.apply(new_data) else: new_data, c = patch.revert(new_data) if c > 0: prefix = "+" if action == "open" else "-" print(f" {prefix} {patch.label} ({c} 处)") total += c if total == 0: print(" 无需修改(已是目标状态或版本不兼容)。") return data if len(new_data) != len(data): print(f" x 严重错误: 文件大小变化 ({len(data)} -> {len(new_data)}),已中止。") return data if not write_safe(inst.target, new_data): return data ok, msg = resign_if_needed(inst.target) if not ok: print(f" x 补丁已写入,但重签名失败: {msg}") elif msg: print(f" {msg}") print(f" 完成,共修改 {total} 处。重启 claude 生效。") return new_datadef interactive(inst: Installation):
data = inst.target.read_bytes()while True: _clear() print("=" * 60) print(" Claude Code Feature Gate 综合补丁工具") print("=" * 60) show_table(data) print(" 命令:") print(" /open 编号 开启 (如: /open 1-5 /open 3 /open 1,3,5)") print(" /close 编号 关闭 (如: /close 11 /close 11-15)") print(" /all 开启全部") print(" /reset 关闭全部(不含白名单类)") print(" /restore 从备份恢复原始文件") print(" q 退出") print() try: cmd = input(" > ").strip() except (EOFError, KeyboardInterrupt): print("\n 已退出。") return cmd_lower = cmd.lower() if cmd_lower in ("q", "quit", "exit"): return if cmd_lower == "/all": targets = [p for p in ALL_PATCHES if p.status(data) != "on"] if not targets: print(" 全部已开启,无需操作。") continue result = _apply_patches(inst, data, targets, "open") if result is not None: data = result continue if cmd_lower == "/reset": targets = [p for p in ALL_PATCHES if p.status(data) == "on" and p.revert_fn is not None and p.category != "白名单"] if not targets: print(" 无可关闭的项。") continue result = _apply_patches(inst, data, targets, "close") if result is not None: data = result continue if cmd_lower == "/restore": restore(inst) data = inst.target.read_bytes() continue # /open 或 /close + 编号 if cmd_lower.startswith("/open"): arg = cmd[5:].strip() indices = _parse_indices(arg) if indices is None: print(" 无效编号。示例: /open 1-5") continue targets = [ALL_PATCHES[i] for i in indices if ALL_PATCHES[i].status(data) != "on"] if not targets: print(" 所选项已全部开启。") continue result = _apply_patches(inst, data, targets, "open") if result is not None: data = result continue if cmd_lower.startswith("/close"): arg = cmd[6:].strip() indices = _parse_indices(arg) if indices is None: print(" 无效编号。示例: /close 11-15") continue targets = [] skipped = [] for i in indices: p = ALL_PATCHES[i] if p.status(data) != "on": continue if p.revert_fn is None: skipped.append(p) continue targets.append(p) for p in skipped: print(f" ~ {p.label}: 不支持字节还原,请用 /restore") if not targets: if not skipped: print(" 所选项已全部关闭。") continue result = _apply_patches(inst, data, targets, "close") if result is not None: data = result continue print(" 未知命令。输入 /open /close /all /reset /restore 或 q")def auto_enable_all(inst: Installation):
“”“自动开启所有功能”“”
data = inst.target.read_bytes()
targets = [p for p in ALL_PATCHES if p.status(data) != “on”]
if not targets:
print(" 全部已开启,无需修改。")
return
_apply_patches(inst, data, targets, “open”)def restore(inst: Installation):
“”“从备份恢复”“”
if not inst.backup.is_file():
print(f" x 未找到备份 {inst.backup}")
returndata = inst.backup.read_bytes() if not write_safe(inst.target, data): return ok, msg = resign_if_needed(inst.target) if not ok: print(f" x 恢复已写入,但重签名失败: {msg}") return if msg: print(f" {msg}") print(" 已从备份恢复。")── 入口 ─────────────────────────────────────────────────────────────
def select_installation(installations: list[Installation]) → Installation | None:
if len(installations) == 1:
inst = installations[0]
print(f"\n 检测到: {inst}")
return instprint(f"\n 检测到 {len(installations)} 个安装:\n") for i, inst in enumerate(installations, 1): print(f" {i}. {inst}") print() while True: try: c = input(" 选择目标 (编号, q 退出): ").strip() except (EOFError, KeyboardInterrupt): return None if c.lower() == "q": return None try: idx = int(c) - 1 if 0 <= idx < len(installations): return installations[idx] except ValueError: pass print(" 无效输入。")def _print_help():
print(“”"
用法: python cs.py [选项]选项:
(无参数) 交互式菜单
–auto 一键开启全部补丁项
–restore 从备份文件恢复原始二进制
–help 显示本帮助信息交互式命令 (输入即执行,立即写入):
/open 编号 开启指定项 /open 3 /open 1-5 /open 1,3,5
/close 编号 关闭指定项 /close 11 /close 11-15
/all 开启全部
/reset 关闭全部功能门控(白名单类不受影响)
/restore 从备份恢复原始文件
q 退出说明:
- 所有补丁均为等长字节替换,不改变文件大小
- 首次修改时自动创建 .features-bak 备份
- ToolSearch 补丁不支持 /close 单项还原,需用 /restore
- macOS 下修改后会自动 ad-hoc 重签名
- 修改后需重启 claude 生效
“”")def main():
if “–help” in sys.argv or “-h” in sys.argv:
_print_help()
return_clear() print("=" * 60) print(" Claude Code Feature Gate 综合补丁工具") print("=" * 60) print(f" 系统: {SYSTEM} | Python {platform.python_version()}") print(f" 补丁项: {len(ALL_PATCHES)} 个 (含 ToolSearch + WebFetch)") print(f" 帮助: python cs.py --help") print() mode = "interactive" for arg in sys.argv[1:]: if arg == "--auto": mode = "auto" elif arg == "--restore": mode = "restore" print(" 正在扫描 Claude Code 安装...") installations = find_all() if not installations: print("\n 未检测到任何 Claude Code 安装。") print(" 支持: bun 官方安装 / npm -g / pnpm add -g") _pause() sys.exit(1) inst = select_installation(installations) if inst is None: print(" 已取消。") _pause() return print() if mode == "restore": restore(inst) elif mode == "auto": auto_enable_all(inst) else: interactive(inst) _pause()def _clear():
os.system(“cls” if os.name == “nt” else “clear”)def _pause():
try:
input(“\n 按回车键退出…”)
except (EOFError, KeyboardInterrupt):
passif name == “main”:
main()


