#!/usr/bin/env node
// hooks/block-env-read.js
// PreToolUse on Read|Grep|Edit|Write|MultiEdit
constchunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',()=>{constt=JSON.parse(Buffer.concat(chunks).toString());constp=t.tool_input?.file_path||t.tool_input?.path||t.tool_input?.pattern||"";constsensitive=['.env','.env.local','.env.production','credentials.json','secrets.yaml','secrets.yml','id_rsa','id_ed25519','.aws/credentials','.gcloud/credentials.json','service-account.json','.npmrc',// 可能包含 token
'.pypirc',// 可能包含 token
];if(sensitive.some(s=>p.includes(s))){console.error(`Blocked: "${p}" matches sensitive pattern. If you need values from this file, ask the user to provide them directly.`);process.exit(2);}process.exit(0);});
Claude: I'll read the environment configuration...
[Hook blocked Read on .env.local]
Claude: I can't read .env.local directly as it contains sensitive data.
Could you share the specific variable names you'd like me to use?
For example: DATABASE_URL, API_KEY, etc.
#!/usr/bin/env node
// hooks/bash-blacklist.js
// PreToolUse on Bash
constchunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',()=>{constt=JSON.parse(Buffer.concat(chunks).toString());constcmd=t.tool_input?.command||"";constbanned=[{re:/rm\s+-rf\s+\/(\s|$)/,desc:"recursive delete from root"},{re:/rm\s+-rf\s+~(\s|$)/,desc:"recursive delete home directory"},{re:/:\(\)\s*\{.*:\|:.*\}\s*;:/,desc:"fork bomb"},{re:/mkfs\./,desc:"filesystem format"},{re:/dd\s+if=.*of=\/dev\//,desc:"raw write to device"},{re:/>\s*\/dev\/sd[a-z]/,desc:"redirect to disk device"},{re:/chmod\s+-R\s+777\s+\//,desc:"world-writable everything"},];for(constbofbanned){if(b.re.test(cmd)){console.error(`Blocked: ${b.desc}. If you really need this, run it yourself.`);process.exit(2);}}process.exit(0);});
#!/usr/bin/env node
// hooks/bash-whitelist.js
// PreToolUse on Bash
constchunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',()=>{constt=JSON.parse(Buffer.concat(chunks).toString());constcmd=(t.tool_input?.command||"").trim();constallow=newSet(['ls','cat','grep','rg','find','head','tail','wc','git','npm','node','python','python3','pip','pip3','curl','jq','echo','pwd','date',]);// 取命令链中的第一个二进制文件名
constfirst=(cmd.split(/\s+/)[0]||"").split('/').pop();if(!allow.has(first)){console.error(`Blocked: "${first}" is not on the allowlist. `+`Allowed: ${[...allow].join(', ')}.`);process.exit(2);}process.exit(0);});
#!/usr/bin/env node
// hooks/block-git-push.js
// PreToolUse on Bash
constchunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',()=>{constt=JSON.parse(Buffer.concat(chunks).toString());constcmd=t.tool_input?.command||"";// 阻止裸 push、force push、push 到远端
constpushPatterns=[/^\s*git\s+push\b/,/git\s+push\s+--force/,/git\s+push\s+-f\b/,/git\s+push\s+\S+\s+\S+/,// git push <remote> <branch>
];if(pushPatterns.some(re=>re.test(cmd))){console.error("Blocked: git push must be human-initiated. "+"Stage and commit if needed, but the push is for me to do.");process.exit(2);}process.exit(0);});
Claude: I'll update the component...
[Edit applied to src/components/Header.tsx]
[PostToolUse hook] Formatted: Header.tsx
Claude: Done. The component now accepts a `subtitle` prop.
#!/usr/bin/env node
// hooks/test-on-edit.js
// PostToolUse on Edit|MultiEdit
const{execSync}=require('child_process');constpath=require('path');constfs=require('fs');constchunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',()=>{constt=JSON.parse(Buffer.concat(chunks).toString());constfilePath=t.tool_input?.file_path||"";// 只对源文件触发,跳过测试与配置
if(!/\/(src|lib|app|packages)\/.*\.(ts|tsx|js|jsx)$/.test(filePath)){process.exit(0);}// 没有 test 脚本就直接放行
constpkgPath=path.resolve('package.json');if(!fs.existsSync(pkgPath))process.exit(0);constpkg=JSON.parse(fs.readFileSync(pkgPath,'utf8'));consthasRelated=pkg.scripts?.['test:related'];consthasTest=pkg.scripts?.test;try{if(hasRelated){// 只跑与改动文件相关的测试
execSync(`npm run -s test:related -- "${filePath}"`,{stdio:'inherit',timeout:60000,});}elseif(hasTest){// 退化为跑完整测试
execSync('npm run -s test',{stdio:'inherit',timeout:120000,});}}catch(e){console.error(`Tests failed after editing ${path.basename(filePath)}.`);console.error("Review the test output above and fix the issue.");process.exit(1);// 1 = 把错误暴露给模型
}process.exit(0);});
Claude: I'll update the validation logic...
[Edit applied to src/validators/email.ts]
[Running test:related for email.ts...]
FAIL tests/validators/email.test.ts
✕ rejects emails without @ symbol (3ms)
Expected: false
Received: true
Tests failed after editing email.ts.
Review the test output above and fix the issue.
Claude: The test shows my regex change broke the @ validation.
Let me fix the pattern...
[Edit applied to src/validators/email.ts]
[Running test:related for email.ts...]
PASS tests/validators/email.test.ts
✓ rejects emails without @ symbol (2ms)
✓ accepts valid emails (1ms)
Claude: Fixed. The regex now correctly requires an @ symbol.
#!/usr/bin/env node
// hooks/read-before-write.js
// PreToolUse on Edit|MultiEdit
// 还需要在 Read 上挂一个伴随条目,用来记录"看过哪些文件"。
constfs=require('fs');constpath=require('path');constchunks=[];process.stdin.on('data',c=>chunks.push(c));process.stdin.on('end',()=>{constt=JSON.parse(Buffer.concat(chunks).toString());constfilePath=t.tool_input?.file_path||t.tool_input?.path||"";constseenFile=path.join('.claude','seen.json');// 加载 seen 映射
letseen={};try{if(fs.existsSync(seenFile)){seen=JSON.parse(fs.readFileSync(seenFile,'utf8'));}}catch{seen={};}// Read 调用:记录并放行
if(t.tool_name==='Read'){seen[filePath]=Date.now();constdir=path.dirname(seenFile);if(!fs.existsSync(dir))fs.mkdirSync(dir,{recursive:true});fs.writeFileSync(seenFile,JSON.stringify(seen,null,2));process.exit(0);}// Edit/MultiEdit:检查文件是否近期被读过(30 分钟内)
constlastSeen=seen[filePath];constthirtyMinutes=30*60*1000;if(!lastSeen){console.error(`Blocked: "${filePath}" has not been Read in this session. `+`Please read the file first to understand its current state before editing.`);process.exit(2);}if(Date.now()-lastSeen>thirtyMinutes){console.error(`Blocked: "${filePath}" was last Read ${Math.round((Date.now()-lastSeen)/60000)} minutes ago. `+`Please re-read the file to verify its current state.`);process.exit(2);}process.exit(0);});