Files
claude-code-skills-reference/tunnel-doctor/scripts/quick_diagnose.py
daymade c49e23e7ef release: v1.38.0 with continue-claude-work and skill-creator enhancements
## New Skill: continue-claude-work (v1.1.0)
- Recover actionable context from local `.claude` session artifacts
- Compact-boundary-aware extraction (reads Claude's own compaction summaries)
- Subagent workflow recovery (reports completed vs interrupted subagents)
- Session end reason detection (clean exit, interrupted, error cascade, abandoned)
- Size-adaptive strategy for small/large sessions
- Noise filtering (skips 37-53% of session lines)
- Self-session exclusion, stale index fallback, MEMORY.md integration
- Bundled Python script (no external dependencies)
- Security scan passed, argument-hint added

## Skill Updates
- **skill-creator** (v1.5.0): Complete rewrite with evaluation framework
  - Added agents/ (analyzer, comparator, grader)
  - Added eval-viewer/ (generate_review.py, viewer.html)
  - Added scripts/ (run_eval, aggregate_benchmark, improve_description, run_loop)
  - Added references/schemas.md (eval/benchmark schemas)
  - Expanded SKILL.md with inline vs fork guidance, progressive disclosure patterns
  - Enhanced package_skill.py and quick_validate.py

- **transcript-fixer** (v1.2.0): CLI improvements and test coverage
  - Enhanced argument_parser.py and commands.py
  - Added correction_service.py improvements
  - Added test_correction_service.py

- **tunnel-doctor** (v1.4.0): Quick diagnostic script
  - Added scripts/quick_diagnose.py
  - Enhanced SKILL.md with 5-layer conflict model

- **pdf-creator** (v1.1.0): Auto DYLD_LIBRARY_PATH + rendering fixes
  - Auto-detect and set DYLD_LIBRARY_PATH for weasyprint
  - Fixed list rendering and CSS improvements

- **github-contributor** (v1.0.3): Enhanced project evaluation
  - Added evidence-loop, redaction, and merge-ready PR guidance

## Documentation
- Updated marketplace.json (v1.38.0, 42 skills)
- Updated CHANGELOG.md with v1.38.0 entry
- Updated CLAUDE.md (skill count, marketplace version, #42 description)
- Updated README.md (badges, skill section #42, use case, requirements)
- Updated README.zh-CN.md (badges, skill section #42, use case, requirements)
- Fixed absolute paths in continue-claude-work/references/file_structure.md

## Validation
- All skills passed quick_validate.py
- continue-claude-work passed security_scan.py
- marketplace.json validated (valid JSON)
- Cross-checked version consistency across all docs
2026-03-07 14:54:33 +08:00

449 lines
14 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Quick tunnel/proxy conflict diagnostics for macOS.
This script detects the most common local and Tailscale networking conflicts:
1) Shell proxy env + NO_PROXY mismatch
2) System proxy exceptions mismatch
3) Proxy path failure vs direct path success
4) Local TLS trust issues
5) Route ownership conflicts for a Tailscale IP (optional)
"""
import argparse
import json
import os
import re
import shlex
import socket
import subprocess
import sys
from typing import Dict, List, Optional, Tuple
def run(cmd: List[str], env: Optional[Dict[str, str]] = None) -> Tuple[int, str, str]:
proc = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=env,
)
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
def split_csv(value: str) -> List[str]:
if not value:
return []
return [item.strip() for item in value.split(",") if item.strip()]
def match_proxy_pattern(host: str, pattern: str) -> bool:
p = pattern.strip().lower()
h = host.strip().lower()
if not p or not h:
return False
# CIDR / wildcard IP patterns are not domain checks.
if "/" in p and not p.startswith("*."):
return False
if re.match(r"^\d+\.\d+\.\d+\.\*$", p):
return False
if p == h:
return True
if p.startswith("*."):
suffix = p[1:] # keep leading dot
return h.endswith(suffix)
if p.startswith("."):
return h.endswith(p)
return False
def has_host_bypass(host: str, patterns: List[str]) -> bool:
return any(match_proxy_pattern(host, item) for item in patterns)
def parse_scutil_proxy() -> Dict[str, object]:
code, stdout, _stderr = run(["scutil", "--proxy"])
if code != 0:
return {"raw": "", "exceptions": [], "http_enabled": False, "https_enabled": False}
raw = stdout
exceptions: List[str] = []
for line in raw.splitlines():
m = re.search(r"\d+\s*:\s*(.+)$", line)
if m and "ExceptionsList" not in line:
exceptions.append(m.group(1).strip())
http_enabled = bool(re.search(r"HTTPEnable\s*:\s*1", raw))
https_enabled = bool(re.search(r"HTTPSEnable\s*:\s*1", raw))
return {
"raw": raw,
"exceptions": exceptions,
"http_enabled": http_enabled,
"https_enabled": https_enabled,
}
def resolve_host(host: str) -> List[str]:
ips = []
try:
infos = socket.getaddrinfo(host, None)
for item in infos:
ip = item[4][0]
if ip not in ips:
ips.append(ip)
except socket.gaierror:
pass
return ips
def curl_status(url: str, timeout: int, mode: str, proxy_url: Optional[str] = None) -> Dict[str, object]:
cmd = [
"curl",
"-k",
"-sS",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"--max-time",
str(timeout),
url,
]
env = os.environ.copy()
if mode == "direct":
for key in (
"http_proxy",
"https_proxy",
"HTTP_PROXY",
"HTTPS_PROXY",
"all_proxy",
"ALL_PROXY",
):
env.pop(key, None)
elif mode == "forced_proxy" and proxy_url:
cmd.extend(["--proxy", proxy_url])
code, stdout, stderr = run(cmd, env=env)
http_code = stdout if stdout else "000"
ok = code == 0 and http_code.isdigit() and http_code != "000"
return {
"ok": ok,
"http_code": http_code,
"exit_code": code,
"stderr": stderr,
"command": " ".join(shlex.quote(x) for x in cmd),
}
def strict_tls_check(url: str, timeout: int) -> Dict[str, object]:
cmd = [
"curl",
"-sS",
"-o",
"/dev/null",
"-w",
"%{http_code}",
"--max-time",
str(timeout),
url,
]
env = os.environ.copy()
for key in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "all_proxy", "ALL_PROXY"):
env.pop(key, None)
code, stdout, stderr = run(cmd, env=env)
cert_issue = "certificate" in stderr.lower() or "ssl" in stderr.lower()
return {
"ok": code == 0 and stdout != "000",
"http_code": stdout if stdout else "000",
"exit_code": code,
"stderr": stderr,
"cert_issue": cert_issue,
}
def route_check(tailscale_ip: str) -> Dict[str, object]:
code, stdout, stderr = run(["route", "-n", "get", tailscale_ip])
if code != 0:
return {"ok": False, "interface": "", "gateway": "", "raw": stderr or stdout}
interface = ""
gateway = ""
for line in stdout.splitlines():
line = line.strip()
if line.startswith("interface:"):
interface = line.split(":", 1)[1].strip()
if line.startswith("gateway:"):
gateway = line.split(":", 1)[1].strip()
return {
"ok": True,
"interface": interface,
"gateway": gateway,
"raw": stdout,
}
def pick_proxy_url() -> Optional[str]:
for key in ("http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"):
value = os.environ.get(key)
if value:
return value
return None
def build_report(
host: str,
url: str,
timeout: int,
tailscale_ip: Optional[str],
) -> Dict[str, object]:
no_proxy = os.environ.get("NO_PROXY", "")
no_proxy_lc = os.environ.get("no_proxy", "")
no_proxy_entries = split_csv(no_proxy if no_proxy else no_proxy_lc)
scutil_info = parse_scutil_proxy()
scutil_exceptions = scutil_info["exceptions"]
proxy_url = pick_proxy_url()
direct = curl_status(url, timeout, mode="direct")
ambient = curl_status(url, timeout, mode="ambient")
forced_proxy = curl_status(url, timeout, mode="forced_proxy", proxy_url=proxy_url) if proxy_url else None
strict_tls = strict_tls_check(url, timeout) if url.startswith("https://") else None
host_ips = resolve_host(host)
host_in_no_proxy = has_host_bypass(host, no_proxy_entries)
host_in_scutil_exceptions = has_host_bypass(host, scutil_exceptions)
findings: List[Dict[str, str]] = []
if not host_ips:
findings.append(
{
"level": "error",
"title": "Host resolution failed",
"detail": f"{host} could not be resolved. Check DNS/hosts first.",
"fix": f"Add a hosts entry if this is local: 127.0.0.1 {host}",
}
)
if proxy_url and not host_in_no_proxy:
findings.append(
{
"level": "warn",
"title": "NO_PROXY missing target host",
"detail": f"Proxy is enabled ({proxy_url}) but NO_PROXY does not match {host}.",
"fix": (
"Add host to NO_PROXY/no_proxy, e.g. "
f"NO_PROXY=...,{host}"
),
}
)
if (scutil_info["http_enabled"] or scutil_info["https_enabled"]) and not host_in_scutil_exceptions:
findings.append(
{
"level": "warn",
"title": "System proxy exception missing target host",
"detail": f"scutil active exceptions do not include {host}.",
"fix": (
"Add host to proxy app skip/bypass list (Shadowrocket/Clash/Surge), "
"then reload profile."
),
}
)
if direct["ok"] and forced_proxy and not forced_proxy["ok"]:
findings.append(
{
"level": "error",
"title": "Proxy path is broken for target host",
"detail": (
"Direct access works, but forced proxy tunnel fails. "
"Traffic must bypass proxy for this host."
),
"fix": (
f"Add {host} to both NO_PROXY and proxy app skip-proxy/DIRECT rules."
),
}
)
if not ambient["ok"] and direct["ok"]:
findings.append(
{
"level": "error",
"title": "Ambient shell path fails while direct path works",
"detail": (
"Current shell env/proxy settings break default access."
),
"fix": (
"Use NO_PROXY for this host, or temporarily unset proxy env for local verification."
),
}
)
if strict_tls and not strict_tls["ok"] and direct["ok"] and strict_tls["cert_issue"]:
findings.append(
{
"level": "warn",
"title": "TLS trust issue detected",
"detail": "Network path is reachable, but strict TLS validation failed.",
"fix": "Trust local CA certificate (for local/internal TLS) or use a valid public cert.",
}
)
route_info = route_check(tailscale_ip) if tailscale_ip else None
if route_info and route_info["ok"]:
iface = str(route_info["interface"])
if iface.startswith("en"):
findings.append(
{
"level": "error",
"title": "Possible route hijack for Tailscale destination",
"detail": f"route -n get {tailscale_ip} resolved to {iface}, not utun*.",
"fix": (
"Check proxy TUN excluded-routes. Do not exclude 100.64.0.0/10 from TUN route table."
),
}
)
summary = {
"host": host,
"url": url,
"host_ips": host_ips,
"proxy_url": proxy_url or "",
"env_no_proxy": no_proxy if no_proxy else no_proxy_lc,
"host_in_no_proxy": host_in_no_proxy,
"scutil_http_enabled": scutil_info["http_enabled"],
"scutil_https_enabled": scutil_info["https_enabled"],
"host_in_scutil_exceptions": host_in_scutil_exceptions,
"connectivity": {
"ambient": ambient,
"direct": direct,
"forced_proxy": forced_proxy,
"strict_tls": strict_tls,
},
"tailscale_route": route_info,
"findings": findings,
}
return summary
def print_human(report: Dict[str, object]) -> int:
print("=== Tunnel Doctor Quick Diagnose ===")
print(f"Host: {report['host']}")
print(f"URL: {report['url']}")
ips = report["host_ips"]
print(f"Resolved IPs: {', '.join(ips) if ips else 'N/A'}")
print("")
print("Proxy Context")
print(f"- proxy env: {report['proxy_url'] or '(not set)'}")
print(f"- host in NO_PROXY: {'yes' if report['host_in_no_proxy'] else 'no'}")
print(
"- system proxy enabled: "
f"HTTP={'yes' if report['scutil_http_enabled'] else 'no'} "
f"HTTPS={'yes' if report['scutil_https_enabled'] else 'no'}"
)
print(f"- host in scutil exceptions: {'yes' if report['host_in_scutil_exceptions'] else 'no'}")
print("")
conn = report["connectivity"]
print("Connectivity Checks")
for key in ("ambient", "direct", "forced_proxy", "strict_tls"):
value = conn.get(key)
if not value:
continue
ok = "PASS" if value.get("ok") else "FAIL"
print(
f"- {key:12s}: {ok} "
f"(http={value.get('http_code', '000')}, exit={value.get('exit_code', 'n/a')})"
)
stderr = value.get("stderr")
if stderr:
print(f" stderr: {stderr}")
print("")
route = report.get("tailscale_route")
if route:
if route.get("ok"):
print("Tailscale Route Check")
print(f"- interface: {route.get('interface') or 'N/A'}")
print(f"- gateway: {route.get('gateway') or 'N/A'}")
print("")
else:
print("Tailscale Route Check")
print(f"- failed: {route.get('raw', '')}")
print("")
findings = report["findings"]
if not findings:
print("Result: no high-confidence conflict found.")
print("If browser still fails, verify proxy app profile mode/rule order and reload profile.")
return 0
print("Findings")
severity_order = {"error": 0, "warn": 1, "info": 2}
findings_sorted = sorted(findings, key=lambda x: severity_order.get(str(x.get("level")), 99))
for idx, item in enumerate(findings_sorted, start=1):
print(f"{idx}. [{item['level'].upper()}] {item['title']}")
print(f" Detail: {item['detail']}")
print(f" Fix: {item['fix']}")
return 1 if any(x["level"] == "error" for x in findings) else 0
def main() -> int:
if sys.platform != "darwin":
print("This script is designed for macOS only.", file=sys.stderr)
return 2
parser = argparse.ArgumentParser(
description="Quick diagnostics for Tailscale + proxy conflicts on macOS."
)
parser.add_argument("--host", default="local.claude4.dev", help="Target host to diagnose.")
parser.add_argument(
"--url",
default="",
help="Full URL to test. Default: https://<host>/health",
)
parser.add_argument(
"--timeout",
type=int,
default=8,
help="curl timeout seconds (default: 8).",
)
parser.add_argument(
"--tailscale-ip",
default="",
help="Optional Tailscale IP for route ownership check (e.g. 100.101.102.103).",
)
parser.add_argument(
"--json",
action="store_true",
help="Output JSON report.",
)
args = parser.parse_args()
url = args.url.strip() or f"https://{args.host}/health"
report = build_report(
host=args.host.strip(),
url=url,
timeout=max(args.timeout, 1),
tailscale_ip=args.tailscale_ip.strip() or None,
)
if args.json:
print(json.dumps(report, indent=2, ensure_ascii=False))
return 0
return print_human(report)
if __name__ == "__main__":
sys.exit(main())