Files
ewoooc/.claude/hooks/commit-quality.js
ogt fcac03379d [V10.4-A] 加強 commit-quality Hook + P9 文件歸檔
新增 Edit/Write/MultiEdit 事件攔截(原僅攔截 git commit Bash 指令),
補齊 getenv fallback 模式偵測,防止 hardcoded Token 透過工具直寫入檔案。

- .claude/hooks/commit-quality.js: 改寫為 PreToolUse JSON 格式,覆蓋 Edit/Write/MultiEdit
- .claude/settings.json: 新增 Edit|Write|MultiEdit|Bash matcher 註冊
- .claude/hooks/__test__/commit-quality.test.sh: 4 case 自動化測試
- docs/guides/DISK_EXPANSION_GUIDE.md: 磁碟擴充 SOP 歸檔
- docs/p9_completion_report_*.md: P9-1 + P9-2 Sprint 完成報告
- docs/refactor/callback_prefix_proposal.md: 308 按鈕回呼前綴分析(Method C)
- docs/refactor/openclaw_bot_routes_split_plan.md: 5999 行神檔拆分計畫

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 01:42:40 +08:00

106 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* commit-quality.js — PreToolUse Hook
* 攔截硬編碼 Secret
* - Bash `git commit`:掃 staged diff保留舊行為
* - Edit / Write / MultiEdit掃 new_string / content / edits[].new_string
* 輸出遵循官方 PreToolUse 規格:
* permissionDecision: "allow" | "deny"
*/
const { spawnSync } = require('child_process');
const SECRET_PATTERNS = [
[/sk-[a-zA-Z0-9]{20,}/, 'OpenAI API Key'],
[/ghp_[a-zA-Z0-9]{36}/, 'GitHub PAT'],
[/AKIA[A-Z0-9]{16}/, 'AWS Access Key'],
[/AIza[a-zA-Z0-9_-]{35}/, 'Google API Key'],
[/\b\d{8,12}:[A-Za-z0-9_-]{35}\b/, 'Telegram Bot Token'],
[/TELEGRAM[_\s]*(?:BOT[_\s]*)?TOKEN\s*=\s*["']?[^\s"']{20,}/, 'Telegram Token 環境變數'],
[/GEMINI_API_KEY\s*=\s*["']?[A-Za-z0-9_-]{20,}/, 'Gemini API Key'],
[/sk-ant-api[0-9a-zA-Z_-]{20,}/, 'Anthropic API Key'],
[/glpat-[a-zA-Z0-9_-]{20}/, 'Gitea/GitLab PAT'],
[/GITEA[_\s]*TOKEN\s*=\s*["']?[^\s"']{20,}/, 'Gitea Token 環境變數'],
// Python fallback default: os.getenv('X', '<secret ≥30 chars>')
[/getenv\s*\(\s*[^,)]+,\s*['"][^'"\s]{30,}['"]\s*\)/, 'Python getenv fallback 含疑似機密'],
[/os\.environ\.get\s*\(\s*[^,)]+,\s*['"][^'"\s]{30,}['"]\s*\)/, 'os.environ.get fallback 含疑似機密'],
];
function scan(text, fileHint) {
if (!text) return [];
const hits = [];
for (const [pat, label] of SECRET_PATTERNS) {
const m = text.match(pat);
if (m) hits.push(`${label}${fileHint ? ` in ${fileHint}` : ''}: ${m[0].slice(0, 24)}...`);
}
// debugger 只對 JS 家族
if (fileHint && /\.(js|jsx|ts|tsx)$/.test(fileHint) && /\bdebugger\b/.test(text)) {
hits.push(`debugger statement in ${fileHint}`);
}
return hits;
}
function allow() {
// 預設 allow輸出官方規格 JSON
const out = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
},
};
process.stdout.write(JSON.stringify(out));
process.exit(0);
}
function deny(reasons) {
const reason = `[commit-quality] 偵測到機密或禁用內容,已阻擋:\n- ${reasons.join('\n- ')}\n請移除後重試。`;
process.stderr.write(reason + '\n');
const out = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: reason,
},
};
process.stdout.write(JSON.stringify(out));
process.exit(0);
}
let d = '';
process.stdin.on('data', c => d += c);
process.stdin.on('end', () => {
try {
const i = JSON.parse(d || '{}');
const tool = i.tool_name || '';
const ti = i.tool_input || {};
const hits = [];
if (tool === 'Bash') {
const cmd = ti.command || '';
if (!/git\s+commit/.test(cmd) || /--amend/.test(cmd)) return allow();
const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8' });
const files = (r.stdout || '').trim().split('\n').filter(Boolean);
for (const f of files) {
if (!/\.(js|jsx|ts|tsx|py|sh|json|yaml|yml|env|toml|ini|conf)$/.test(f)) continue;
const cr = spawnSync('git', ['show', ':' + f], { encoding: 'utf8' });
hits.push(...scan(cr.stdout || '', f));
}
} else if (tool === 'Write') {
hits.push(...scan(ti.content || '', ti.file_path));
} else if (tool === 'Edit') {
hits.push(...scan(ti.new_string || '', ti.file_path));
} else if (tool === 'MultiEdit') {
const edits = Array.isArray(ti.edits) ? ti.edits : [];
for (const e of edits) hits.push(...scan(e.new_string || '', ti.file_path));
} else {
return allow();
}
if (hits.length) return deny(hits);
return allow();
} catch (e) {
process.stderr.write(`[commit-quality] hook internal error: ${e.message}\n`);
// 內部錯誤不阻擋,避免誤殺
return allow();
}
});