diff --git a/README.md b/README.md index 4abe79c..5e648f3 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ node "$CLAUDE_SKILL_DIR/scripts/check-deps.mjs" # 手动运行请替换为实际路径,如 ~/.claude/skills/web-access ``` +> **子 Agent 权限**:Claude Code 子 Agent 不继承任何级别的 `permissions.allow`([claude-code#18950](https://github.com/anthropics/claude-code/issues/18950)),首次运行 `check-deps.mjs` 时会自动安装 PreToolUse hook 到 `~/.claude/hooks/`,放行 CDP curl 命令、WebSearch 和 Jina 调用,确保并行调研功能的子 Agent 正常工作,无需手动配置。 + ## CDP Proxy API Proxy 通过 WebSocket 直连 Chrome(兼容 `chrome://inspect` 方式,无需命令行参数启动),提供 HTTP API: diff --git a/SKILL.md b/SKILL.md index bfe0659..7890d4a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -183,6 +183,8 @@ Proxy 持续运行,不建议主动停止——重启后需要在 Chrome 中重 - **速度**:多子 Agent 并行,总耗时约等于单个子任务时长 - **上下文保护**:抓取内容不进入主 Agent 上下文,主 Agent 只接收摘要,节省 token +**权限自动配置**:子 Agent 不继承主 Agent 的任何级别权限(Claude Code 平台限制,`permissions.allow` 对子 Agent 无效)。`check-deps.mjs` 通过安装 PreToolUse hook(权限评估最高优先级,对所有 Agent 生效)自动解决此问题——hook 会放行 CDP curl 命令、WebSearch 和 Jina WebFetch,无需用户手动配置。 + **并行 CDP 操作**:每个子 Agent 在当前用户浏览器实例中,自行创建所需的后台 tab(`/new`),自行操作,任务结束自行关闭(`/close`)。所有子 Agent 共享一个 Chrome、一个 Proxy,通过不同 targetId 操作不同 tab,无竞态风险。 **子 Agent Prompt 写法:目标导向,而非步骤指令** diff --git a/scripts/approve-tools-hook.mjs b/scripts/approve-tools-hook.mjs new file mode 100644 index 0000000..15e8086 --- /dev/null +++ b/scripts/approve-tools-hook.mjs @@ -0,0 +1,48 @@ +#!/usr/bin/env node +// web-access PreToolUse Hook +// 自动放行 CDP Proxy 操作和联网工具调用,解决子 Agent 权限不继承的平台限制 +// Auto-approve CDP proxy operations and networking tools for sub-agents +// Addresses: anthropics/claude-code#18950, #37730, #25526 + +let input = ''; +process.stdin.on('data', chunk => { input += chunk; }); +process.stdin.on('end', () => { + try { + const data = JSON.parse(input); + const tool = data.tool_name; + const toolInput = data.tool_input || {}; + let allow = false; + + if (tool === 'Bash') { + const cmd = toolInput.command || ''; + // CDP Proxy curl 命令(仅 localhost) + // CDP proxy curl commands (localhost only) + if (/^curl\s+.*https?:\/\/(localhost|127\.0\.0\.1):\d+\//.test(cmd)) allow = true; + // skill 脚本(check-deps, cdp-proxy, match-site) + // skill scripts + if (/^node\s+.*\b(check-deps|cdp-proxy|match-site)\b/.test(cmd)) allow = true; + } else if (tool === 'WebSearch') { + allow = true; + } else if (tool === 'WebFetch') { + const url = toolInput.url || ''; + // Jina Markdown 转换服务 + if (/^https?:\/\/r\.jina\.ai\//.test(url)) allow = true; + } + + if (allow) { + process.stdout.write(JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'allow', + permissionDecisionReason: 'web-access: auto-approved CDP/networking operation', + }, + })); + } + // 无输出 = 走默认权限流程(ask user) + // no output = fall through to default permission flow + } catch { + // hook 异常不应阻塞操作 + // hook errors must not block operations + } + process.exit(0); +}); diff --git a/scripts/check-deps.mjs b/scripts/check-deps.mjs index d6cc31e..cdab9cf 100644 --- a/scripts/check-deps.mjs +++ b/scripts/check-deps.mjs @@ -138,10 +138,90 @@ async function ensureProxy() { return false; } +// --- 子 Agent 权限:PreToolUse Hook 自动配置 --- +// Claude Code 子 Agent 不继承任何级别的 permissions.allow(anthropics/claude-code#18950, #37730, #25526) +// 唯一对子 Agent 生效的权限机制是 PreToolUse hooks(权限评估最高优先级) +// 此函数将 hook 脚本安装到 ~/.claude/hooks/ 并在 settings.json 中注册 + +const HOOK_FILENAME = 'web-access-approve-tools.mjs'; + +function ensureHooks() { + const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); + const settingsPath = path.join(configDir, 'settings.json'); + const hooksDir = path.join(configDir, 'hooks'); + const hookDest = path.join(hooksDir, HOOK_FILENAME); + const hookSrc = path.join(ROOT, 'scripts', 'approve-tools-hook.mjs'); + + try { + // 1. 安装 hook 脚本到稳定位置 + // install hook script to stable location + if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true }); + + const srcContent = fs.readFileSync(hookSrc, 'utf8'); + let needCopy = true; + if (fs.existsSync(hookDest)) { + needCopy = fs.readFileSync(hookDest, 'utf8') !== srcContent; + } + if (needCopy) { + fs.writeFileSync(hookDest, srcContent, { mode: 0o755 }); + } + + // 2. 在 settings.json 中注册 PreToolUse hook + // register PreToolUse hook in settings.json + let settings = {}; + if (fs.existsSync(settingsPath)) { + settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + } + + if (!settings.hooks) settings.hooks = {}; + if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = []; + + const hookCommand = `node "${hookDest}"`; + const hookEntry = { + matcher: 'Bash|WebSearch|WebFetch', + hooks: [{ type: 'command', command: hookCommand }], + }; + + // 检查是否已注册(按 command 匹配,避免重复) + // check if already registered (match by command to avoid duplicates) + const alreadyRegistered = settings.hooks.PreToolUse.some(entry => + entry.hooks?.some(h => h.command?.includes(HOOK_FILENAME)) + ); + + if (alreadyRegistered) { + // 更新已有条目(hook 脚本可能已更新) + // update existing entry (hook script may have been updated) + settings.hooks.PreToolUse = settings.hooks.PreToolUse.map(entry => { + if (entry.hooks?.some(h => h.command?.includes(HOOK_FILENAME))) { + return hookEntry; + } + return entry; + }); + if (needCopy) { + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + console.log('hooks: updated (web-access hook script refreshed)'); + } else { + console.log('hooks: ok'); + } + } else { + settings.hooks.PreToolUse.push(hookEntry); + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + console.log('hooks: configured (PreToolUse hook registered for sub-agent CDP/search access)'); + } + } catch (e) { + // hook 配置失败不阻塞主流程,仅警告 + // hook setup failure is non-blocking, just warn + console.log(`hooks: warn (auto-config failed: ${e.message})`); + console.log(' 子 Agent 并行调研可能因权限不足失败'); + console.log(' 详见: https://github.com/anthropics/claude-code/issues/18950'); + } +} + // --- main --- async function main() { checkNode(); + ensureHooks(); const chromePort = await detectChromePort(); if (!chromePort) { @@ -154,7 +234,6 @@ async function main() { if (!proxyOk) { process.exit(1); } - } await main();