claude cli 限制功能解除

参考 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), count

def _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 items

ALL_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 fallback

class 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 results

def _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})")]
return

def _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})")]
return

def 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 True

def 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 w

def _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.category

    cur = 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 None

def _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_data

def 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}")
return

data = 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 inst

print(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):
pass

if name == “main”:
main()

19 Likes

前排支持!

不错啊!这就试试!

感谢分享,格式有点问题佬~可以传github或者传文件看看

2 Likes

:joy: 直接叫 AI 修复下把,哈哈

1 Like

好长啊。。

最新的2.1.72 claude code,是不能用这个脚本强制开启Tool Search

1 Like

直接在setting中"ENABLE_TOOL_SEARCH": "auto"就行

新版还没看的。哈哈

但其他功能好像都能正常更改,还是谢谢提供修改思路 :tieba_013:

打算用魔法去打败魔法防御? :joy:

我这边加了没用么,2.1.72,难道是姿势错误?



刚刚测试了,应该是可以的

image
下面的token数也说明是正常运行

感谢截图,没放入env里,可以了。再问下佬友这个看token的是什么插件。谢谢

哈雷的ccline,直接论坛搜索

1 Like

整一个…

我这边要用true才可以,只用auto还是没有

下面是官方文档

懂了,原来是我工具蛮多auto才会自动生效,工具太少了就没必要

auto模式下对非官方api也能因为工具多而生效吗