#!/usr/bin/env node /** * PreToolUse Hook: NDA 读取防护 (standalone — 零外部 require) * 匹配器: Read|Glob|Grep * 退出码: 0=放行, 2=阻断(deny) * * 此文件内联了所有依赖,terser 混淆后无 require 链性能损失。 * 源码维护版: nda-read-guard.js (含 checkInput 导出供测试) * 构建时由 build-portable.js 用此文件替换 nda-read-guard.js */ 'use strict'; const fs = require('fs'); const path = require('path'); // --- 内联: root.js --- const CLAUDE_ROOT = (() => { if (process.env.CLAUDE_HOME) return process.env.CLAUDE_HOME; const selfDir = path.dirname(__filename); if (selfDir.includes('.claude')) return selfDir.replace(/[\/\\]hooks$/, ''); return path.join(process.env.USERPROFILE || process.env.HOME || '', '.claude'); })(); // --- 内联: logSecurityEvent (精简版, fail-open) --- function logSecurityEvent(decision, reason, detail) { try { const dd = path.join(CLAUDE_ROOT, 'debug'); if (!fs.existsSync(dd)) fs.mkdirSync(dd, { recursive: true }); const lf = path.join(dd, 'security-' + new Date().toISOString().slice(0, 10) + '.jsonl'); fs.appendFileSync(lf, JSON.stringify({ ts: new Date().toISOString(), decision, hook: 'nda-read-guard', reason, detail: (detail || '').slice(0, 200).replace(/[A-Za-z0-9+/]{32,}/g, '[REDACTED]'), }) + '\n'); } catch {} } // --- 路径规范化 --- function norm(fp) { if (!fp) return ''; fp = fp.trim(); let r = path.resolve(fp); try { r = fs.realpathSync(r); } catch {} return r.replace(/\\/g, '/').replace(/\.+$/, '').replace(/::?\$DATA$/i, ''); } // --- 白名单 --- const WL = [ /[\/]\.claude[\/]projects([\/]|$)/i, /[\/]\.claude[\/]memory([\/]|$)/i, ]; // --- 黑名单 --- const BL = [ /[\/]\.claude[\/]CLAUDE\.md$/i, /[\/]\.claude[\/]settings\.json$/i, /[\/]\.claude[\/]settings\.local\.json$/i, /[\/]\.claude[\/]stats-compiled\.json$/i, /[\/]\.claude[\/]MEMORY\.md$/i, /[\/]\.claude[\/]skills[\/]/i, /[\/]\.claude[\/]agents[\/]/i, /[\/]\.claude[\/]hooks[\/]/i, /[\/]\.claude[\/]scripts[\/]/i, /[\/]\.claude[\/]rules[\/]/i, /[\/]\.claude[\/]constitution[\/]/i, /[\/]\.claude[\/]docs[\/]/i, /[\/]\.claude[\/]debug[\/]/i, /[\/]\.claude[\/]templates[\/]/i, ]; // --- Glob 模式黑名单 --- const GL = [ /\.claude/i, /skills[\/\\]/i, /agents[\/\\]/i, /hooks[\/\\]/i, /CLAUDE\.md/i, /settings\.json/i, /SKILL\.md/i, ]; const DENY_MSG = '[系统] 该路径属于系统内部区域,无法访问。请直接描述您需要完成的任务,我来帮您。'; function chkPath(np) { if (!np) return null; for (const w of WL) { if (w.test(np)) return null; } for (const b of BL) { if (b.test(np)) return 1; } if (/[\/]\.claude[\/]/.test(np)) return 1; return null; } function chkGlob(p) { if (!p) return null; for (const g of GL) { if (g.test(p)) return 1; } return null; } // --- 内联: readStdin --- function main() { let raw = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', c => { raw += c; if (raw.length > 512 * 1024) { process.exit(0); } }); process.stdin.on('end', () => { try { const input = JSON.parse(raw); const tn = (input.tool_name || '').toLowerCase(); const ti = input.tool_input || {}; let hit = null; let target = ''; if (tn === 'read') { target = ti.file_path || ti.filePath || ''; hit = chkPath(norm(target)); } else if (tn === 'glob') { target = ti.path || ''; hit = chkPath(norm(target)) || chkGlob(ti.pattern || ''); if (!target && hit) target = ti.pattern; } else if (tn === 'grep') { target = ti.path || ''; hit = chkPath(norm(target)); } if (hit) { logSecurityEvent('nda-deny', 'blocked', target); process.stderr.write(JSON.stringify({ hookSpecificOutput: { permissionDecision: 'deny' }, systemMessage: DENY_MSG, })); process.exit(2); return; } } catch {} process.exit(0); }); } main();