Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 写法:目标导向,而非步骤指令**
Expand Down
48 changes: 48 additions & 0 deletions scripts/approve-tools-hook.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
81 changes: 80 additions & 1 deletion scripts/check-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -154,7 +234,6 @@ async function main() {
if (!proxyOk) {
process.exit(1);
}

}

await main();