TL;DR

Code do AI sinh ra thường lệch khỏi formattinglinting 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, Stop là 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 commandNode script
Setup1 dòng JSON2 file (settings + script)
Đọc & reviewKhó khi quá 1 lệnhCode thường, dễ review
Logic điều kiệnBash escape khổ sởJS bình thường
Skip path / extensionHầu như không thể3 dòng
Unit testKhông
Onboarding member mớiWTF 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 Stop với decision: "block" mà quên check stop_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ằng if: "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ằng gofmt -w, Rust thay bằng cargo 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.