From 3bae87901127f21fe0a3391b950ab402ab631bc5 Mon Sep 17 00:00:00 2001 From: Jianyu Lin Date: Fri, 3 Apr 2026 12:18:45 +0800 Subject: [PATCH 1/2] fix: auto-configure sub-agent permissions for parallel research Claude Code sub-agents don't inherit session-level permissions from settings.local.json (anthropics/claude-code#18950), causing parallel research tasks to fail when sub-agents try to use Bash (CDP curl), WebSearch, or WebFetch. This adds an `ensurePermissions()` step to `check-deps.mjs` that automatically writes the required permissions to `~/.claude/settings.json` (global scope, inherited by all agents including sub-agents). The function is idempotent, non-blocking on failure, and handles fresh installs. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 ++ SKILL.md | 2 ++ scripts/check-deps.mjs | 51 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4abe79c..8ddb560 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ node "$CLAUDE_SKILL_DIR/scripts/check-deps.mjs" # 手动运行请替换为实际路径,如 ~/.claude/skills/web-access ``` +> **子 Agent 权限**:首次运行 `check-deps.mjs` 时,脚本会自动将并行调研所需的权限(CDP curl 命令、WebSearch、Jina 等)写入 `~/.claude/settings.json`。这解决了 Claude Code 子 Agent 不继承会话级权限的平台限制([claude-code#18950](https://github.com/anthropics/claude-code/issues/18950)),无需手动配置。 + ## CDP Proxy API Proxy 通过 WebSocket 直连 Chrome(兼容 `chrome://inspect` 方式,无需命令行参数启动),提供 HTTP API: diff --git a/SKILL.md b/SKILL.md index bfe0659..9db0743 100644 --- a/SKILL.md +++ b/SKILL.md @@ -183,6 +183,8 @@ Proxy 持续运行,不建议主动停止——重启后需要在 Chrome 中重 - **速度**:多子 Agent 并行,总耗时约等于单个子任务时长 - **上下文保护**:抓取内容不进入主 Agent 上下文,主 Agent 只接收摘要,节省 token +**权限自动配置**:子 Agent 不继承主 Agent 的会话级权限(Claude Code 平台限制),但 `check-deps.mjs` 会自动将 CDP 操作和联网工具的权限写入 `~/.claude/settings.json`(全局权限对所有 Agent 生效),确保子 Agent 可直接执行 curl、WebSearch 等操作,无需用户手动配置。 + **并行 CDP 操作**:每个子 Agent 在当前用户浏览器实例中,自行创建所需的后台 tab(`/new`),自行操作,任务结束自行关闭(`/close`)。所有子 Agent 共享一个 Chrome、一个 Proxy,通过不同 targetId 操作不同 tab,无竞态风险。 **子 Agent Prompt 写法:目标导向,而非步骤指令** diff --git a/scripts/check-deps.mjs b/scripts/check-deps.mjs index d6cc31e..020d6a8 100644 --- a/scripts/check-deps.mjs +++ b/scripts/check-deps.mjs @@ -138,10 +138,60 @@ async function ensureProxy() { return false; } +// --- 子 Agent 权限自动配置 --- +// Claude Code 子 Agent 不继承 settings.local.json 的会话级权限(anthropics/claude-code#18950) +// 但 settings.json 中的全局权限对所有 Agent(包括子 Agent)生效 +// 此函数确保并行调研所需的权限已配置,避免子 Agent 因权限被拒而失败 + +const REQUIRED_PERMISSIONS = [ + 'Bash(curl -s http://localhost:3456/*)', + 'Bash(curl -s -X POST "http://localhost:3456/*)', + 'Bash(node *check-deps*)', + 'Bash(node *cdp-proxy*)', + 'Bash(node "$CLAUDE_SKILL_DIR/*")', + 'WebSearch', + 'WebFetch(domain:r.jina.ai)', +]; + +function ensurePermissions() { + const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); + const settingsPath = path.join(configDir, 'settings.json'); + + try { + let settings = {}; + if (fs.existsSync(settingsPath)) { + settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + } + + // 合并权限:只添加缺失项,不影响已有配置 + // merge permissions: only add missing entries, never remove existing ones + if (!settings.permissions) settings.permissions = {}; + if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = []; + + const existing = new Set(settings.permissions.allow); + const missing = REQUIRED_PERMISSIONS.filter(p => !existing.has(p)); + + if (missing.length === 0) { + console.log('permissions: ok'); + return; + } + + settings.permissions.allow.push(...missing); + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); + console.log(`permissions: configured (added ${missing.length} rules for sub-agent CDP/search access)`); + } catch (e) { + // 权限配置失败不阻塞主流程,仅警告 + // permission setup failure is non-blocking, just warn + console.log(`permissions: warn (auto-config failed: ${e.message})`); + console.log(' 子 Agent 并行调研可能因权限不足失败,可手动配置 ~/.claude/settings.json'); + } +} + // --- main --- async function main() { checkNode(); + ensurePermissions(); const chromePort = await detectChromePort(); if (!chromePort) { @@ -154,7 +204,6 @@ async function main() { if (!proxyOk) { process.exit(1); } - } await main(); From ff353e1c7918986241099a2d642e24fcb5d687d6 Mon Sep 17 00:00:00 2001 From: Jianyu Lin Date: Fri, 3 Apr 2026 13:10:39 +0800 Subject: [PATCH 2/2] fix: use PreToolUse hooks instead of permissions.allow for sub-agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initial approach (writing to permissions.allow in settings.json) was incorrect — sub-agents don't inherit permissions.allow at any scope level (anthropics/claude-code#18950, #37730, #25526). PreToolUse hooks are the only permission mechanism that works for sub-agents (highest priority in the permission evaluation flow). This commit: - Adds approve-tools-hook.mjs: auto-approves CDP curl (localhost only), skill scripts, WebSearch, and Jina WebFetch - Replaces ensurePermissions() with ensureHooks() in check-deps.mjs: installs the hook to ~/.claude/hooks/ and registers it in settings.json - Hook is idempotent, non-blocking on failure, auto-refreshes on update Tested with 45 E2E tests covering: all CDP endpoints, dangerous command rejection, WebSearch/WebFetch conditional approval, edge cases, idempotency, existing config preservation, and auto-refresh. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- SKILL.md | 2 +- scripts/approve-tools-hook.mjs | 48 ++++++++++++++++ scripts/check-deps.mjs | 102 +++++++++++++++++++++------------ 4 files changed, 116 insertions(+), 38 deletions(-) create mode 100644 scripts/approve-tools-hook.mjs diff --git a/README.md b/README.md index 8ddb560..5e648f3 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ node "$CLAUDE_SKILL_DIR/scripts/check-deps.mjs" # 手动运行请替换为实际路径,如 ~/.claude/skills/web-access ``` -> **子 Agent 权限**:首次运行 `check-deps.mjs` 时,脚本会自动将并行调研所需的权限(CDP curl 命令、WebSearch、Jina 等)写入 `~/.claude/settings.json`。这解决了 Claude Code 子 Agent 不继承会话级权限的平台限制([claude-code#18950](https://github.com/anthropics/claude-code/issues/18950)),无需手动配置。 +> **子 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 diff --git a/SKILL.md b/SKILL.md index 9db0743..7890d4a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -183,7 +183,7 @@ Proxy 持续运行,不建议主动停止——重启后需要在 Chrome 中重 - **速度**:多子 Agent 并行,总耗时约等于单个子任务时长 - **上下文保护**:抓取内容不进入主 Agent 上下文,主 Agent 只接收摘要,节省 token -**权限自动配置**:子 Agent 不继承主 Agent 的会话级权限(Claude Code 平台限制),但 `check-deps.mjs` 会自动将 CDP 操作和联网工具的权限写入 `~/.claude/settings.json`(全局权限对所有 Agent 生效),确保子 Agent 可直接执行 curl、WebSearch 等操作,无需用户手动配置。 +**权限自动配置**:子 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,无竞态风险。 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 020d6a8..cdab9cf 100644 --- a/scripts/check-deps.mjs +++ b/scripts/check-deps.mjs @@ -138,52 +138,82 @@ async function ensureProxy() { return false; } -// --- 子 Agent 权限自动配置 --- -// Claude Code 子 Agent 不继承 settings.local.json 的会话级权限(anthropics/claude-code#18950) -// 但 settings.json 中的全局权限对所有 Agent(包括子 Agent)生效 -// 此函数确保并行调研所需的权限已配置,避免子 Agent 因权限被拒而失败 - -const REQUIRED_PERMISSIONS = [ - 'Bash(curl -s http://localhost:3456/*)', - 'Bash(curl -s -X POST "http://localhost:3456/*)', - 'Bash(node *check-deps*)', - 'Bash(node *cdp-proxy*)', - 'Bash(node "$CLAUDE_SKILL_DIR/*")', - 'WebSearch', - 'WebFetch(domain:r.jina.ai)', -]; - -function ensurePermissions() { +// --- 子 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')); } - // 合并权限:只添加缺失项,不影响已有配置 - // merge permissions: only add missing entries, never remove existing ones - if (!settings.permissions) settings.permissions = {}; - if (!Array.isArray(settings.permissions.allow)) settings.permissions.allow = []; - - const existing = new Set(settings.permissions.allow); - const missing = REQUIRED_PERMISSIONS.filter(p => !existing.has(p)); - - if (missing.length === 0) { - console.log('permissions: ok'); - return; + 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)'); } - - settings.permissions.allow.push(...missing); - fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); - console.log(`permissions: configured (added ${missing.length} rules for sub-agent CDP/search access)`); } catch (e) { - // 权限配置失败不阻塞主流程,仅警告 - // permission setup failure is non-blocking, just warn - console.log(`permissions: warn (auto-config failed: ${e.message})`); - console.log(' 子 Agent 并行调研可能因权限不足失败,可手动配置 ~/.claude/settings.json'); + // 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'); } } @@ -191,7 +221,7 @@ function ensurePermissions() { async function main() { checkNode(); - ensurePermissions(); + ensureHooks(); const chromePort = await detectChromePort(); if (!chromePort) {