From c1146ab6b699d2b92ea194ecc4cb9fe53dc803ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?=
<31078449+Nowhitestar@users.noreply.github.com>
Date: Mon, 29 Jun 2026 22:42:04 +0800
Subject: [PATCH 1/2] feat(telemetry): report uninstall completion
---
README.md | 4 +-
SECURITY.md | 3 +-
docs/README_zh.md | 4 +-
scripts/dev-smoke.sh | 60 ++++++++++++++-
scripts/uninstall.ps1 | 168 +++++++++++++++++++++++++++++++++++++++++-
scripts/uninstall.sh | 167 ++++++++++++++++++++++++++++++++++++++++-
6 files changed, 395 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 246ea9a..cb2c6b1 100644
--- a/README.md
+++ b/README.md
@@ -125,7 +125,7 @@ Underlying providers are routed automatically and grow over time — your agent
Is it safe?
-Yes. AgentKey is a master key — one platform that unlocks external capabilities for your agent. By design, we have no access to your local files, your credentials, or your agent's conversations. The only data AgentKey collects is anonymous usage telemetry — which agent you installed into, your skill version, and upgrade outcomes — never your queries or responses. See "How do I opt out of telemetry?" below.
+Yes. AgentKey is a master key — one platform that unlocks external capabilities for your agent. By design, we have no access to your local files, your credentials, or your agent's conversations. The only data AgentKey collects is anonymous usage telemetry — which agent you installed into or removed from, your skill version, and upgrade outcomes — never your queries or responses. See "How do I opt out of telemetry?" below.
@@ -230,7 +230,7 @@ The one-command uninstaller additionally cleans npm/npx caches, legacy shell rc
How do I opt out of telemetry?
-AgentKey sends anonymous usage telemetry (which agent you use, skill version, upgrade outcomes — never queries or responses). Three ways to opt out, any of them works:
+AgentKey sends anonymous usage telemetry (which agent you install into or remove from, skill version, upgrade outcomes — never queries or responses). Three ways to opt out, any of them works:
```bash
# Persistent opt-out (recommended)
diff --git a/SECURITY.md b/SECURITY.md
index 97d8dcb..055b110 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -56,12 +56,13 @@ The skill verifies MCP health by calling the MCP `list_tools` endpoint directly
|---|---|---|
| `api.github.com` | At most every 24 hours | Look up the latest release tag |
| npm registry | When the user accepts a skill update | Resolve and run `npx skills update agentkey` |
+| `api.agentkey.app` | On install, uninstall, and best-effort skill/update checks unless telemetry is disabled | Anonymous usage telemetry and update metadata |
### Credential handling
- `AGENTKEY_API_KEY` is stored only in user-local config files (paths above).
- The key leaves the user's machine only as the `Authorization` header to AgentKey's own API endpoints.
-- The skill collects no telemetry.
+- Telemetry is anonymous and limited to install/uninstall/update metadata; it never includes queries, responses, or local file contents.
### Supply chain
diff --git a/docs/README_zh.md b/docs/README_zh.md
index 1811d09..77faeee 100644
--- a/docs/README_zh.md
+++ b/docs/README_zh.md
@@ -125,7 +125,7 @@ AgentKey 在云端维护与开放互联网各类平台的对接 —— 你不需
安全吗?
-安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们看不到你的本地文件、凭证或 Agent 的对话。AgentKey 只采集匿名使用统计 —— 你装到了哪些 Agent、Skill 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"。
+安全。AgentKey 是 Agent 的"万能钥匙"—— 一个平台帮你的 Agent 解锁外部能力。按架构设计,我们看不到你的本地文件、凭证或 Agent 的对话。AgentKey 只采集匿名使用统计 —— 你装到或移除了哪些 Agent、Skill 版本、升级结果 —— 永远不采集你的查询内容或返回数据。详见下方"我如何关闭遥测?"。
@@ -230,7 +230,7 @@ npx skills remove chainbase-labs/agentkey
我如何关闭遥测?
-AgentKey 会上报匿名使用统计(你用的 Agent、Skill 版本、升级结果 —— 永远不会上报查询内容或返回数据)。任选一种方式关闭:
+AgentKey 会上报匿名使用统计(你安装到或移除的 Agent、Skill 版本、升级结果 —— 永远不会上报查询内容或返回数据)。任选一种方式关闭:
```bash
# 持久关闭(推荐)
diff --git a/scripts/dev-smoke.sh b/scripts/dev-smoke.sh
index b46df9a..e5decbd 100755
--- a/scripts/dev-smoke.sh
+++ b/scripts/dev-smoke.sh
@@ -390,6 +390,48 @@ command = "legacy"
key = "val"
EOF
+ # Mock telemetry endpoint: uninstaller should post once before scrubbing
+ # configs, but telemetry must remain best-effort and isolated to sandbox.
+ local telemetry_log="$SANDBOX/telemetry.json"
+ local telemetry_port_file="$SANDBOX/telemetry-port"
+ python3 -u - "$telemetry_log" "$telemetry_port_file" <<'PY' &
+import http.server
+import json
+import sys
+
+log_path, port_path = sys.argv[1], sys.argv[2]
+
+class Handler(http.server.BaseHTTPRequestHandler):
+ def do_POST(self):
+ length = int(self.headers.get("Content-Length") or "0")
+ body = self.rfile.read(length).decode("utf-8")
+ with open(log_path, "w", encoding="utf-8") as f:
+ f.write(json.dumps({
+ "path": self.path,
+ "authorization": self.headers.get("Authorization"),
+ "body": body,
+ }))
+ self.send_response(204)
+ self.end_headers()
+
+ def log_message(self, *_):
+ pass
+
+server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
+with open(port_path, "w", encoding="utf-8") as f:
+ f.write(str(server.server_port))
+server.timeout = 10
+server.handle_request()
+PY
+ local telemetry_pid=$!
+ local _wait
+ for _wait in {1..50}; do
+ [ -s "$telemetry_port_file" ] && break
+ sleep 0.1
+ done
+ local telemetry_port
+ telemetry_port="$(cat "$telemetry_port_file" 2>/dev/null || true)"
+
# Gemini: claude-code per-project shape simulator (nested).
cat > "$SANDBOX/.claude.json" <<'EOF'
{
@@ -407,8 +449,24 @@ EOF
# ── Run the uninstaller in the sandbox ────────────────────────────────
info "running uninstall.sh --skip-skill-remove --force-in-repo"
- HOME="$SANDBOX" bash "$SKILL_REPO/scripts/uninstall.sh" \
+ AGENTKEY_BASE_URL="http://127.0.0.1:$telemetry_port" \
+ HOME="$SANDBOX" bash "$SKILL_REPO/scripts/uninstall.sh" \
--skip-skill-remove --force-in-repo >/dev/null 2>&1 || true
+ for _wait in {1..50}; do
+ [ -s "$telemetry_log" ] && break
+ sleep 0.1
+ done
+ kill "$telemetry_pid" 2>/dev/null || true
+ wait "$telemetry_pid" 2>/dev/null || true
+
+ local telemetry_body
+ telemetry_body="$(cat "$telemetry_log" 2>/dev/null || true)"
+ assert_contains "uninstall telemetry endpoint called" \
+ "/v1/telemetry/uninstall-completed" "$telemetry_body"
+ assert_contains "uninstall telemetry uses stored API key" \
+ "Bearer ak_xxx" "$telemetry_body"
+ assert_contains "uninstall telemetry includes removed codex agent" \
+ "codex" "$telemetry_body"
# ── Assertions: agentkey gone, decoys preserved ───────────────────────
info "checking: agentkey entries scrubbed"
diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1
index 041f12b..4624de3 100644
--- a/scripts/uninstall.ps1
+++ b/scripts/uninstall.ps1
@@ -49,6 +49,170 @@ function Write-Warn2($t) { Write-Host " ! $t" -ForegroundColor Yellow }
function Write-Skip ($t) { Write-Host " - $t" -ForegroundColor DarkGray }
function Write-Err ($t) { Write-Host " ✗ $t" -ForegroundColor Red }
+$home2 = [Environment]::GetFolderPath('UserProfile')
+$UninstallerFlags = @($PSBoundParameters.Keys | ForEach-Object { "-$_" })
+
+function Send-UninstallTelemetry {
+ try {
+ if ($env:AGENTKEY_TELEMETRY -eq '0') { return }
+ $optOut = Join-Path $home2 '.config\agentkey\telemetry-disabled'
+ if (Test-Path -LiteralPath $optOut) { return }
+
+ $script:TelemetryApiKey = $null
+ $script:TelemetryAgents = @()
+ $serverNames = @('agentkey', 'agentkey.app agentkey')
+
+ function Add-TelemetryAgent($id) {
+ if ($script:TelemetryAgents -notcontains $id) {
+ $script:TelemetryAgents += $id
+ }
+ }
+
+ function Read-TelemetryEntry($id, $entry) {
+ Add-TelemetryAgent $id
+ if ($null -eq $entry -or $script:TelemetryApiKey) { return }
+ try {
+ foreach ($field in @('headers', 'http_headers', 'env', 'environment')) {
+ $bagProp = $entry.PSObject.Properties[$field]
+ if ($null -eq $bagProp -or $null -eq $bagProp.Value) { continue }
+ $bag = $bagProp.Value
+ if ($bag.PSObject.Properties['Authorization']) {
+ $auth = [string]$bag.Authorization
+ if ($auth -match '(?i)\bBearer\s+(.+)$') {
+ $script:TelemetryApiKey = $Matches[1].Trim()
+ return
+ }
+ }
+ if ($bag.PSObject.Properties['AGENTKEY_API_KEY']) {
+ $script:TelemetryApiKey = ([string]$bag.AGENTKEY_API_KEY).Trim()
+ return
+ }
+ }
+ } catch {}
+ }
+
+ function Walk-TelemetryJson($id, $node) {
+ if ($null -eq $node) { return }
+ if ($node -is [System.Management.Automation.PSCustomObject]) {
+ foreach ($prop in @($node.PSObject.Properties)) {
+ if ($serverNames -contains $prop.Name.ToLower()) {
+ Read-TelemetryEntry $id $prop.Value
+ } else {
+ Walk-TelemetryJson $id $prop.Value
+ }
+ }
+ } elseif ($node -is [System.Collections.IEnumerable] -and -not ($node -is [string])) {
+ foreach ($item in $node) { Walk-TelemetryJson $id $item }
+ }
+ }
+
+ $jsonConfigs = @(
+ @{ Id = 'claude-code'; Path = (Join-Path $home2 '.claude.json') },
+ @{ Id = 'cursor'; Path = (Join-Path $home2 '.cursor\mcp.json') },
+ @{ Id = 'claude-desktop'; Path = (Join-Path $env:APPDATA 'Claude\claude_desktop_config.json') },
+ @{ Id = 'gemini-cli'; Path = (Join-Path $home2 '.gemini\settings.json') },
+ @{ Id = 'qwen-code'; Path = (Join-Path $home2 '.qwen\settings.json') },
+ @{ Id = 'iflow-cli'; Path = (Join-Path $home2 '.iflow\settings.json') },
+ @{ Id = 'kimi-cli'; Path = (Join-Path $home2 '.kimi\mcp.json') },
+ @{ Id = 'kiro-cli'; Path = (Join-Path $home2 '.kiro\settings\mcp.json') },
+ @{ Id = 'windsurf'; Path = (Join-Path $home2 '.codeium\windsurf\mcp_config.json') },
+ @{ Id = 'warp'; Path = (Join-Path $home2 '.warp\.mcp.json') },
+ @{ Id = 'opencode'; Path = (Join-Path $env:APPDATA 'opencode\opencode.json') },
+ @{ Id = 'amp'; Path = (Join-Path $env:APPDATA 'amp\settings.json') },
+ @{ Id = 'crush'; Path = (Join-Path $env:APPDATA 'crush\crush.json') }
+ )
+ foreach ($cfg in $jsonConfigs) {
+ if (-not (Test-Path -LiteralPath $cfg.Path)) { continue }
+ try {
+ Walk-TelemetryJson $cfg.Id (Get-Content $cfg.Path -Raw | ConvertFrom-Json)
+ } catch {}
+ }
+
+ $codexToml = Join-Path $home2 '.codex\config.toml'
+ if (Test-Path -LiteralPath $codexToml) {
+ $inBlock = $false
+ foreach ($line in Get-Content $codexToml) {
+ if ($line -match '^\s*\[\s*mcp_servers\s*\.\s*(agentkey|"agentkey\.app AgentKey")(\.[^\]]+)?\s*\]\s*$') {
+ $inBlock = $true
+ Add-TelemetryAgent 'codex'
+ continue
+ }
+ if ($line -match '^\s*\[[^\]]+\]\s*$') { $inBlock = $false }
+ if (-not $inBlock -or $script:TelemetryApiKey) { continue }
+ if ($line -match 'Authorization\s*=\s*"Bearer\s+([^"]+)"') {
+ $script:TelemetryApiKey = $Matches[1].Trim()
+ } elseif ($line -match 'AGENTKEY_API_KEY\s*=\s*"([^"]+)"') {
+ $script:TelemetryApiKey = $Matches[1].Trim()
+ }
+ }
+ }
+
+ if (-not $script:TelemetryApiKey -or $script:TelemetryAgents.Count -eq 0) { return }
+
+ $skillVersion = ''
+ $versionCandidates = @(
+ '.agents\skills\agentkey\version.txt',
+ '.claude\skills\agentkey\version.txt',
+ '.cursor\skills\agentkey\version.txt',
+ '.codex\skills\agentkey\version.txt',
+ '.gemini\skills\agentkey\version.txt',
+ '.opencode\skills\agentkey\version.txt',
+ '.openclaw\skills\agentkey\version.txt',
+ '.qwen\skills\agentkey\version.txt',
+ '.iflow\skills\agentkey\version.txt',
+ '.windsurf\skills\agentkey\version.txt',
+ '.warp\skills\agentkey\version.txt',
+ '.kimi\skills\agentkey\version.txt',
+ '.kiro\skills\agentkey\version.txt'
+ )
+ foreach ($rel in $versionCandidates) {
+ $p = Join-Path $home2 $rel
+ if (Test-Path -LiteralPath $p) {
+ $skillVersion = (Get-Content $p -Raw).Trim()
+ break
+ }
+ }
+ foreach ($rel in @(
+ 'amp\skills\agentkey\version.txt',
+ 'crush\skills\agentkey\version.txt',
+ 'goose\skills\agentkey\version.txt',
+ 'opencode\skills\agentkey\version.txt'
+ )) {
+ if ($skillVersion) { break }
+ $p = Join-Path $env:APPDATA $rel
+ if (Test-Path -LiteralPath $p) {
+ $skillVersion = (Get-Content $p -Raw).Trim()
+ }
+ }
+
+ $_hn = [System.Net.Dns]::GetHostName()
+ $_user = $env:USERNAME
+ $_bytes = [System.Text.Encoding]::UTF8.GetBytes("$_hn|windows|$_user")
+ $_sha = [System.Security.Cryptography.SHA256]::Create()
+ $_hash = ($_sha.ComputeHash($_bytes) | ForEach-Object { $_.ToString('x2') }) -join ''
+ $baseUrl = if ($env:AGENTKEY_BASE_URL) { $env:AGENTKEY_BASE_URL.TrimEnd('/') } else { 'https://api.agentkey.app' }
+ $payload = @{
+ properties = @{
+ device_fingerprint = $_hash.Substring(0, 16)
+ uninstall_source = 'one_liner'
+ removed_agents = @($script:TelemetryAgents | Sort-Object -Unique)
+ uninstaller_flags = $script:UninstallerFlags
+ skill_version = $skillVersion
+ os = 'win32'
+ os_arch = $env:PROCESSOR_ARCHITECTURE
+ }
+ } | ConvertTo-Json -Depth 10
+
+ Invoke-WebRequest -Uri "$baseUrl/v1/telemetry/uninstall-completed" `
+ -Method Post `
+ -Headers @{ Authorization = "Bearer $script:TelemetryApiKey" } `
+ -ContentType 'application/json' `
+ -Body $payload `
+ -TimeoutSec 2 `
+ -UseBasicParsing | Out-Null
+ } catch {}
+}
+
# ── Safety rail ──────────────────────────────────────────────────────────
if ((Test-Path '.claude-plugin/plugin.json') -and -not $ForceInRepo) {
$content = Get-Content '.claude-plugin/plugin.json' -Raw -ErrorAction SilentlyContinue
@@ -68,6 +232,8 @@ Write-Host ''
Write-Host ' AgentKey — Uninstall' -ForegroundColor White
Write-Host ' https://agentkey.app' -ForegroundColor DarkGray
+Send-UninstallTelemetry
+
# ── 1. Skill removal via skills CLI ──────────────────────────────────────
Write-Step '1. Skill files'
@@ -94,8 +260,6 @@ if ($SkipSkillRemove) {
# ── 2. MCP config cleanup ────────────────────────────────────────────────
Write-Step '2. MCP server entries'
-$home2 = [Environment]::GetFolderPath('UserProfile')
-
# All known JSON MCP config paths across the 16 auto-supported agents. The
# scrub logic is schema-agnostic — it walks the JSON tree and drops any
# dict key whose name exactly matches our server name (current + legacy),
diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh
index 283ff12..fb0ce6b 100755
--- a/scripts/uninstall.sh
+++ b/scripts/uninstall.sh
@@ -56,6 +56,168 @@ warn() { printf " ${WARN}!${NC} %s\n" "$*"; }
skipped() { printf " ${MUTED}-${NC} %s\n" "$*"; }
step() { printf "\n ${BOLD}%s${NC}\n" "$*"; }
+have_python() { command -v python3 >/dev/null 2>&1 || command -v python >/dev/null 2>&1; }
+py() { if command -v python3 >/dev/null 2>&1; then python3 "$@"; else python "$@"; fi; }
+
+send_uninstall_telemetry() {
+ [ "${AGENTKEY_TELEMETRY:-1}" = "0" ] && return 0
+ [ -f "$HOME/.config/agentkey/telemetry-disabled" ] && return 0
+ have_python || return 0
+
+ AGENTKEY_UNINSTALLER_FLAGS="$*" py <<'PY' >/dev/null 2>&1 || true
+import hashlib
+import json
+import os
+import platform
+import re
+import socket
+import sys
+import urllib.request
+
+HOME = os.path.expanduser("~")
+SERVER_NAMES = {"agentkey", "agentkey.app agentkey"}
+DEFAULT_BASE_URL = "https://api.agentkey.app"
+
+json_configs = [
+ ("claude-code", os.path.join(HOME, ".claude.json")),
+ ("cursor", os.path.join(HOME, ".cursor", "mcp.json")),
+ ("gemini-cli", os.path.join(HOME, ".gemini", "settings.json")),
+ ("qwen-code", os.path.join(HOME, ".qwen", "settings.json")),
+ ("iflow-cli", os.path.join(HOME, ".iflow", "settings.json")),
+ ("kimi-cli", os.path.join(HOME, ".kimi", "mcp.json")),
+ ("kiro-cli", os.path.join(HOME, ".kiro", "settings", "mcp.json")),
+ ("windsurf", os.path.join(HOME, ".codeium", "windsurf", "mcp_config.json")),
+ ("warp", os.path.join(HOME, ".warp", ".mcp.json")),
+ ("opencode", os.path.join(HOME, ".config", "opencode", "opencode.json")),
+ ("amp", os.path.join(HOME, ".config", "amp", "settings.json")),
+ ("crush", os.path.join(HOME, ".config", "crush", "crush.json")),
+]
+if sys.platform == "darwin":
+ json_configs.append(("claude-desktop", os.path.join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json")))
+else:
+ json_configs.append(("claude-desktop", os.path.join(HOME, ".config", "Claude", "claude_desktop_config.json")))
+
+toml_configs = [("codex", os.path.join(HOME, ".codex", "config.toml"))]
+
+api_key = ""
+removed_agents = set()
+
+def remember(agent_id, entry):
+ global api_key
+ removed_agents.add(agent_id)
+ if not isinstance(entry, dict) or api_key:
+ return
+ for field in ("headers", "http_headers", "env", "environment"):
+ bag = entry.get(field)
+ if not isinstance(bag, dict):
+ continue
+ auth = bag.get("Authorization") or bag.get("authorization")
+ if isinstance(auth, str):
+ m = re.search(r"\bBearer\s+(.+)$", auth.strip(), re.I)
+ if m:
+ api_key = m.group(1).strip()
+ return
+ val = bag.get("AGENTKEY_API_KEY")
+ if isinstance(val, str) and val:
+ api_key = val.strip()
+ return
+
+def walk_json(agent_id, node):
+ if isinstance(node, dict):
+ for key, value in node.items():
+ if key.lower() in SERVER_NAMES:
+ remember(agent_id, value)
+ else:
+ walk_json(agent_id, value)
+ elif isinstance(node, list):
+ for item in node:
+ walk_json(agent_id, item)
+
+for agent_id, path in json_configs:
+ try:
+ with open(path, encoding="utf-8") as f:
+ walk_json(agent_id, json.load(f))
+ except Exception:
+ pass
+
+agent_header = re.compile(r'^\s*\[\s*mcp_servers\s*\.\s*(agentkey|"agentkey\.app AgentKey")(\.[^\]]+)?\s*\]\s*$')
+any_header = re.compile(r"^\s*\[[^\]]+\]\s*$")
+for agent_id, path in toml_configs:
+ try:
+ in_block = False
+ with open(path, encoding="utf-8") as f:
+ for line in f:
+ if agent_header.match(line):
+ in_block = True
+ removed_agents.add(agent_id)
+ continue
+ if any_header.match(line):
+ in_block = False
+ if not in_block or api_key:
+ continue
+ m = re.search(r'Authorization\s*=\s*"Bearer\s+([^"]+)"', line)
+ if not m:
+ m = re.search(r'AGENTKEY_API_KEY\s*=\s*"([^"]+)"', line)
+ if m:
+ api_key = m.group(1).strip()
+ except Exception:
+ pass
+
+if not api_key or not removed_agents:
+ raise SystemExit(0)
+
+skill_version = ""
+for rel in (
+ ".agents/skills/agentkey/version.txt",
+ ".claude/skills/agentkey/version.txt",
+ ".cursor/skills/agentkey/version.txt",
+ ".codex/skills/agentkey/version.txt",
+ ".gemini/skills/agentkey/version.txt",
+ ".opencode/skills/agentkey/version.txt",
+ ".openclaw/skills/agentkey/version.txt",
+ ".qwen/skills/agentkey/version.txt",
+ ".iflow/skills/agentkey/version.txt",
+ ".windsurf/skills/agentkey/version.txt",
+ ".warp/skills/agentkey/version.txt",
+ ".kimi/skills/agentkey/version.txt",
+ ".kiro/skills/agentkey/version.txt",
+ ".config/amp/skills/agentkey/version.txt",
+ ".config/crush/skills/agentkey/version.txt",
+ ".config/goose/skills/agentkey/version.txt",
+):
+ try:
+ with open(os.path.join(HOME, rel), encoding="utf-8") as f:
+ skill_version = f.read().strip()
+ break
+ except Exception:
+ pass
+
+os_name = "macos" if sys.platform == "darwin" else "linux"
+user = os.environ.get("USER") or ""
+fingerprint_input = f"{socket.gethostname()}|{os_name}|{user}"
+device_fingerprint = hashlib.sha256(fingerprint_input.encode()).hexdigest()[:16]
+base_url = (os.environ.get("AGENTKEY_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
+payload = {
+ "properties": {
+ "device_fingerprint": device_fingerprint,
+ "uninstall_source": "one_liner",
+ "removed_agents": sorted(removed_agents),
+ "uninstaller_flags": [x for x in os.environ.get("AGENTKEY_UNINSTALLER_FLAGS", "").split() if x],
+ "skill_version": skill_version,
+ "os": sys.platform,
+ "os_arch": platform.machine(),
+ }
+}
+req = urllib.request.Request(
+ f"{base_url}/v1/telemetry/uninstall-completed",
+ data=json.dumps(payload).encode("utf-8"),
+ headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
+ method="POST",
+)
+urllib.request.urlopen(req, timeout=2).read()
+PY
+}
+
# ── Safety rail ──────────────────────────────────────────────────────────
if [ -f ".claude-plugin/plugin.json" ] \
&& grep -q '"name"[[:space:]]*:[[:space:]]*"agentkey"' .claude-plugin/plugin.json 2>/dev/null \
@@ -70,6 +232,8 @@ fi
printf "\n ${BOLD}AgentKey — Uninstall${NC}\n"
printf " ${MUTED}https://agentkey.app${NC}\n"
+send_uninstall_telemetry "$@"
+
# ── 1. Remove the skill via skills CLI ────────────────────────────────────
step "1. Skill files"
@@ -133,9 +297,6 @@ MCP_TOML_CONFIGS=(
"$HOME/.codex/config.toml" # Codex CLI
)
-have_python() { command -v python3 >/dev/null 2>&1 || command -v python >/dev/null 2>&1; }
-py() { if command -v python3 >/dev/null 2>&1; then python3 "$@"; else python "$@"; fi; }
-
if ! have_python; then
warn "python not found — skipping JSON cleanup; edit these files manually:"
for f in "${MCP_JSON_CONFIGS[@]}"; do [ -f "$f" ] && echo " $f"; done
From b79561370d7c330c9cf702ba40e1b8b69d3d2098 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=8D=E7=99=BD?=
<31078449+Nowhitestar@users.noreply.github.com>
Date: Mon, 6 Jul 2026 12:15:48 +0800
Subject: [PATCH 2/2] fix(telemetry): ignore placeholder uninstall keys
---
scripts/dev-smoke.sh | 6 +++++-
scripts/uninstall.ps1 | 18 ++++++++++++------
scripts/uninstall.sh | 19 +++++++++++++------
3 files changed, 30 insertions(+), 13 deletions(-)
diff --git a/scripts/dev-smoke.sh b/scripts/dev-smoke.sh
index e5decbd..7d98dc3 100755
--- a/scripts/dev-smoke.sh
+++ b/scripts/dev-smoke.sh
@@ -332,7 +332,11 @@ phase_4() {
cat > "$SANDBOX/.cursor/mcp.json" <<'EOF'
{
"mcpServers": {
- "agentkey": { "command": "npx", "args": ["-y", "@agentkey/cli"] },
+ "agentkey": {
+ "command": "npx",
+ "args": ["-y", "@agentkey/cli"],
+ "headers": { "Authorization": "Bearer ${user_config.AGENTKEY_API_KEY}" }
+ },
"agentkey-helper": { "command": "x" },
"other-svr": { "command": "y" }
}
diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1
index 4624de3..f5279d6 100644
--- a/scripts/uninstall.ps1
+++ b/scripts/uninstall.ps1
@@ -68,6 +68,14 @@ function Send-UninstallTelemetry {
}
}
+ function Set-TelemetryApiKey($value) {
+ if ($null -eq $value) { return $false }
+ $v = ([string]$value).Trim()
+ if (-not $v.StartsWith('ak_')) { return $false }
+ $script:TelemetryApiKey = $v
+ return $true
+ }
+
function Read-TelemetryEntry($id, $entry) {
Add-TelemetryAgent $id
if ($null -eq $entry -or $script:TelemetryApiKey) { return }
@@ -79,13 +87,11 @@ function Send-UninstallTelemetry {
if ($bag.PSObject.Properties['Authorization']) {
$auth = [string]$bag.Authorization
if ($auth -match '(?i)\bBearer\s+(.+)$') {
- $script:TelemetryApiKey = $Matches[1].Trim()
- return
+ if (Set-TelemetryApiKey $Matches[1]) { return }
}
}
if ($bag.PSObject.Properties['AGENTKEY_API_KEY']) {
- $script:TelemetryApiKey = ([string]$bag.AGENTKEY_API_KEY).Trim()
- return
+ if (Set-TelemetryApiKey $bag.AGENTKEY_API_KEY) { return }
}
}
} catch {}
@@ -140,9 +146,9 @@ function Send-UninstallTelemetry {
if ($line -match '^\s*\[[^\]]+\]\s*$') { $inBlock = $false }
if (-not $inBlock -or $script:TelemetryApiKey) { continue }
if ($line -match 'Authorization\s*=\s*"Bearer\s+([^"]+)"') {
- $script:TelemetryApiKey = $Matches[1].Trim()
+ Set-TelemetryApiKey $Matches[1] | Out-Null
} elseif ($line -match 'AGENTKEY_API_KEY\s*=\s*"([^"]+)"') {
- $script:TelemetryApiKey = $Matches[1].Trim()
+ Set-TelemetryApiKey $Matches[1] | Out-Null
}
}
}
diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh
index fb0ce6b..cae80b2 100755
--- a/scripts/uninstall.sh
+++ b/scripts/uninstall.sh
@@ -102,8 +102,17 @@ toml_configs = [("codex", os.path.join(HOME, ".codex", "config.toml"))]
api_key = ""
removed_agents = set()
-def remember(agent_id, entry):
+def set_api_key(value):
global api_key
+ if not isinstance(value, str):
+ return False
+ value = value.strip()
+ if not value.startswith("ak_"):
+ return False
+ api_key = value
+ return True
+
+def remember(agent_id, entry):
removed_agents.add(agent_id)
if not isinstance(entry, dict) or api_key:
return
@@ -114,12 +123,10 @@ def remember(agent_id, entry):
auth = bag.get("Authorization") or bag.get("authorization")
if isinstance(auth, str):
m = re.search(r"\bBearer\s+(.+)$", auth.strip(), re.I)
- if m:
- api_key = m.group(1).strip()
+ if m and set_api_key(m.group(1)):
return
val = bag.get("AGENTKEY_API_KEY")
- if isinstance(val, str) and val:
- api_key = val.strip()
+ if set_api_key(val):
return
def walk_json(agent_id, node):
@@ -159,7 +166,7 @@ for agent_id, path in toml_configs:
if not m:
m = re.search(r'AGENTKEY_API_KEY\s*=\s*"([^"]+)"', line)
if m:
- api_key = m.group(1).strip()
+ set_api_key(m.group(1))
except Exception:
pass