- Cory House chia sẻ một workflow tip ngắn: thay vì để AI sinh code lệch chuẩn rồi đi sửa tay, hãy gắn một hook chạy Prettier + ESLint ngay sau mỗi lần Claude Code edit file — và đặt logic vào một Node script để dễ đọc, dễ mở rộng.
TL;DR
Code do AI sinh ra thường lệch khỏi formatting và linting standards của team. Cory House (@housecor) đề xuất một fix cực gọn cho người dùng Claude Code: cấu hình một hook tự động chạy prettier --write + eslint --fix sau mỗi lần Claude edit file. Mẹo nhỏ thêm: thay vì nhồi câu lệnh inline vào settings.json, đặt nội dung hook trong một Node script để dễ đọc, dễ document, dễ thêm logic (skip node_modules/, log, branch theo extension...).
Vấn đề thực tế
Bạn cài Prettier, ESLint, husky, lint-staged đầy đủ. Team có .prettierrc chốt hai-space, .eslintrc chốt no-unused-vars. Rồi Claude Code chen vào, viết một loạt component dùng tab, dấu nháy đôi, import thừa. Diff PR đầy noise, reviewer phải nhắc style thay vì soi logic — hoặc bạn ngồi npm run lint -- --fix thủ công sau mỗi prompt.
Vòng lặp đó vô lý: máy tự sinh code thì cũng nên để máy tự fix style.
Giải pháp: hook PostToolUse
Claude Code có hệ thống hooks ra mắt từ 6/2025, đến đầu 2026 đã lên tới 17–21 lifecycle events. Event đúng cho ca này là PostToolUse với matcher Edit|Write — fire ngay sau khi tool edit file chạy xong, nhận stdin JSON chứa tool_input.file_path.
Lưu ý thuật ngữ: snippet gốc của Cory gọi nôm na là "stop hook". Trong taxonomy chính thức của Claude Code,
Stoplà event fire khi Claude kết thúc cả lượt trả lời (hợp cho final type-check). Còn "format ngay sau mỗi edit" làPostToolUse. Hai cái khác nhau, nên gắn đúng để tránh chạy thừa.
Cấu hình tối thiểu (inline)
Đặt vào .claude/settings.json trong repo (commit được, share cả team):
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$(jq -r '.tool_input.file_path')\" && npx eslint --fix \"$(jq -r '.tool_input.file_path')\""
}
]
}
]
}
}Một dòng. Mọi file Claude edit đều bị Prettier rồi ESLint nhào nặn lại trước khi bạn nhìn diff.
Tách sang Node script — đây là điểm Cory nhấn mạnh
Inline ổn cho 1 lệnh. Nhưng đời thật cần: skip node_modules/, .next/, dist/; chỉ chạy với .ts/.tsx/.js/.json/.md; log lỗi; có khi cần env riêng. Nhồi hết vào string trong JSON là tự gây khó cho mình. Cách Cory chọn: gọi node với một file script độc lập.
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/format-on-edit.js\"",
"timeout": 60
}
]
}
]
}
}.claude/hooks/format-on-edit.js:
const fs = require('fs');
const { execSync } = require('child_process');
const path = require('path');
const event = JSON.parse(fs.readFileSync(0, 'utf8'));
const filePath = event.tool_input?.file_path;
if (!filePath) process.exit(0);
const skip = ['node_modules/', '.next/', 'dist/', 'build/', '.git/'];
if (skip.some(s => filePath.includes(s))) process.exit(0);
const ext = path.extname(filePath);
const supported = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md'];
if (!supported.includes(ext)) process.exit(0);
try { execSync(`npx prettier --write "${filePath}"`, { stdio: 'pipe' }); } catch {}
try { execSync(`npx eslint --fix "${filePath}"`, { stdio: 'pipe' }); }
catch (e) { console.error(e.stdout?.toString() || e.message); process.exit(0); }Bây giờ logic là code thật — diff PR review được, đồng đội mở ra hiểu ngay, thêm rule mới chỉ là sửa file .js.
So sánh nhanh inline vs script
| Tiêu chí | Inline command | Node script |
|---|---|---|
| Setup | 1 dòng JSON | 2 file (settings + script) |
| Đọc & review | Khó khi quá 1 lệnh | Code thường, dễ review |
| Logic điều kiện | Bash escape khổ sở | JS bình thường |
| Skip path / extension | Hầu như không thể | 3 dòng |
| Unit test | Không | Có |
| Onboarding member mới | WTF moment | Đọc là hiểu |
Lưu ý & gotchas
- Exit code 2 ở PostToolUse không block tool (tool đã chạy xong rồi) — nhưng stderr sẽ được feed lại cho Claude, dùng để báo "lint còn lỗi, fix nốt đi".
- Tránh dùng
Stopvớidecision: "block"mà quên checkstop_hook_active— bạn sẽ tự tạo vòng lặp vô hạn. - Latency. Repo lớn có thể tốn 1–3s mỗi edit. Mẹo: thêm
async: true,timeout, hoặc giới hạn matcher bằngif: "Edit(src/**)". - Prettier vs ESLint cãi nhau: cài
eslint-config-prettierđể tắt các rule style trùng. - Generalize được: Python thay bằng
ruff format && ruff check --fix, Go thay bằnggofmt -w, Rust thay bằngcargo fmt && cargo clippy --fix.
Chốt lại
Đây là dạng tip 5 phút setup, lợi cả tháng: AI viết code, hook nhồi style, bạn chỉ review logic. Bí quyết của Cory không nằm ở việc biết Claude Code có hook — ai cũng biết — mà ở quyết định nhỏ tách logic ra Node script. Code-as-config thay vì string-soup-as-config. Một thói quen kỷ luật rất nhỏ, nhưng giữ cho hệ tự động không thoái hoá thành đống cmd rối rắm sau 3 tháng.
Nguồn: Cory House on X, Claude Code Hooks reference, Pixelmojo — All 12 Hook Events 2026.

