Tự động Review Code

Một bản triển khai tham chiếu có sẵn tại
redline.

Các tính năng chính:

  • Claude (hoặc người dùng) quyết định khi nào cần review
  • Claude Code toàn quyền kiểm soát reviewer agent — nó chạy như một tiến trình nền bất đồng bộ.
  • Cả hai agent đều có thể quan sát, tùy biến và giám sát chi phí ngay trên Trạm AI
  • Đăng nhập cả hai agent chỉ bằng một lệnh: redline login
  • Được triển khai dưới dạng một stop hook duy nhất của Claude Code, mang lại sự minh bạch và khả năng tùy biến đối với agent cùng (các) model được sử dụng

Mục tiêu đạt được

Mỗi khi Claude Code hoàn tất một phản hồi mà vẫn còn
thay đổi chưa commit, một hook sẽ tự động kích hoạt
một bản review code Codex chạy nền — không hề chặn
quy trình làm việc. Người dùng vẫn làm việc bình thường
trong lúc bản review chạy. Khi hoàn tất, Claude đọc kết quả
và trình bày các phát hiện.

lines
Claude Code Stop event
→ redline check (fast, <1s)
→ git diff --stat HEAD (any uncommitted changes?)
→ hash diff stat, compare to .git/redline-last-diff
→ if changes exist AND diff has changed since last check:
save hash to .git/redline-last-diff
output { "decision": "block", "reason": "..." }
reason includes diff stat summary
Claude decides if changes warrant a review
Claude spawns `redline review` as background task
→ codex exec review streams output in real-time
→ user can monitor, kill, or keep working
→ when done, Claude reads output, presents findings
→ if no changes OR same diff as last check:
exit 0 silently → Claude proceeds normally

Không có tiến trình nền, không daemon, không giao thức filesystem. Hook chính là trigger, còn hệ thống background task của riêng Claude Code lo phần thực thi bất đồng bộ.

Yêu cầu trước

Cấu hình Trạm AI

Cả hai agent đều route inference qua Trạm AI, nhưng mỗi agent dùng một API skin và base URL khác nhau.

Claude Code

Thiết lập các biến môi trường này trong shell profile.
(~/.zshrc, ~/.bashrc). Không sử dụng file .env
— Claude Code không đọc chúng.

lines
$export ANTHROPIC_BASE_URL="https://api.staging.tram.ai.vn"
$export ANTHROPIC_AUTH_TOKEN="tram_sk-..."
$export ANTHROPIC_API_KEY=""

Base URL là https://api.staging.tram.ai.vnkhông có hậu tố /v1. Đây là Anthropic Skin của Trạm AI, vốn giao tiếp bằng giao thức Anthropic gốc. Việc dùng /v1 sẽ gây ra lỗi model-not-found. ANTHROPIC_API_KEY phải được để trống một cách rõ ràng để ngăn Claude Code xác thực trực tiếp với Anthropic.

Kiểm tra bằng cách chạy /status trong một phiên Claude Code. Xem hướng dẫn tích hợp Claude Code đầy đủ để biết chi tiết.

Codex CLI

Tạo hoặc chỉnh sửa ~/.codex/config.toml:

lines
1[model_providers.tram]
2name = "Tram AI"
3base_url = "https://api.staging.tram.ai.vn/v1"
4env_key = "TRAM_AI_API_KEY"

Sau đó thiết lập API key:

lines
$export TRAM_AI_API_KEY="tram_sk-..."

Khi chạy, truyền -c 'model_provider="tram"' để chọn provider Trạm AI.

Các lỗi thường gặp:

  • Codex CLI không có flag --provider — dùng -c cho mọi config override khi chạy
  • Mục TOML là [model_providers.tram], không phải [provider.tram]
  • Codex dùng https://api.staging.tram.ai.vn/v1 (có /v1), trong khi Claude dùng https://api.staging.tram.ai.vn (không có /v1) — chúng dùng các protocol skin khác nhau

Xem hướng dẫn tích hợp Codex CLI đầy đủ để biết chi tiết.

Tìm hiểu Stop Hook

Claude Code có một hệ thống hook được cấu hình trong settings.json. Hook quan trọng cho trường hợp này là Stop — nó kích hoạt mỗi khi Claude hoàn tất một chu kỳ phản hồi.

decision: "block" hoạt động như thế nào

  1. Lệnh hook chạy và xuất JSON ra stdout
  2. Nếu JSON chứa "decision": "block" cùng với một
    chuỗi "reason", Claude Code:
    • Không dừng lại — nó tiếp tục cuộc hội thoại
    • Văn bản reason được đưa vào context của Claude
      như một thông tin mới
    • Claude xử lý và hành động theo đó (ví dụ,
      spawn một background task theo chỉ dẫn)
  3. Nếu lệnh thoát với mã 0 và không có output JSON, Claude
    tiếp tục bình thường (không chặn)
  4. Nếu lệnh thoát với mã khác 0, nó được coi là một
    lỗi không chặn

Đây chính là cơ chế then chốt: lệnh check dùng
decision: "block" để đưa bản tóm tắt diff stat
và các chỉ dẫn review vào context của Claude. Claude
thấy bản tóm tắt, tự đánh giá xem các thay đổi có đáng
review hay không, và nếu có thì spawn lệnh review qua
công cụ Bash của nó. Background task này hiện ngay trong
danh sách task của Claude — người dùng có thể nhìn thấy, theo dõi và hủy.

Vị trí các file cài đặt

Theo thứ tự ưu tiên:

  • ~/.claude/settings.json — toàn cục (tất cả project)
  • .claude/settings.json — theo project (có thể chia sẻ,
    được commit)
  • .claude/settings.local.json — theo project (cục bộ,
    bị gitignore)

Dùng .claude/settings.local.json cho review hook để nó không ảnh hưởng đến các cộng tác viên khác.

Cấu hình hook

lines
1{
2 "hooks": {
3 "Stop": [
4 {
5 "hooks": [
6 {
7 "type": "command",
8 "command": "redline check",
9 "timeout": 10
10 }
11 ]
12 }
13 ]
14 }
15}

Timeout đặt 10 giây — bước check rất nhanh. Con số này
thấp hơn nhiều so với 300 giây sẽ cần cho một
bản review đồng bộ.

Vì sao bất đồng bộ lại quan trọng

Cách làm đồng bộ thì đơn giản hơn — chạy toàn bộ bản review ngay bên trong Stop hook — nhưng vướng ba vấn đề:

  1. Chặn.codex exec review có thể mất vài phút.
    Một Stop hook đồng bộ sẽ chặn Claude Code suốt
    thời gian đó — không thấy được tiến độ, không có cách
    để hủy.
  2. Không lọc. Hook kích hoạt sau mỗi phản hồi,
    kể cả khi Claude vừa trả lời một câu hỏi
    mà không thực hiện thay đổi code nào.
  3. Review trùng lặp. Không có gì ngăn hook
    kích hoạt một bản review thứ hai trong khi một bản
    đang chạy.

Cách làm bất đồng bộ xử lý được cả ba: bước check nhanh (<1s), chỉ kích hoạt khi có thay đổi, hash diff stat để bỏ qua khi không có gì thay đổi kể từ lần check trước, và văn bản reason chỉ dẫn Claude bỏ qua nếu đã có một bản review đang chạy. Bản review thật sự thì chạy như một background task của Claude Code, và người dùng có thể theo dõi hoặc hủy nó.

Kiến trúc hai lệnh

Công cụ được chia thành hai lệnh: check (cổng kiểm tra nhanh, do hook gọi) và review (công việc thật sự, do Claude spawn như một background task).

Xây dựng lệnh check

Lệnh check chạy trên mỗi sự kiện Stop và phải hoàn tất trong chưa đầy một giây.

expandable lines
1import { execSync } from "child_process";
2import {
3 existsSync,
4 readFileSync,
5 writeFileSync,
6} from "fs";
7import { join } from "path";
8
9function getDiffStat(): string {
10 // Prefer diff --stat for a concise summary
11 const diff = execSync("git diff --stat HEAD", {
12 encoding: "utf-8",
13 }).trim();
14 if (diff) return diff;
15
16 // Fall back to status for untracked files
17 return execSync("git status --porcelain", {
18 encoding: "utf-8",
19 }).trim();
20}
21
22function hash(s: string): string {
23 let h = 0;
24 for (let i = 0; i < s.length; i++) {
25 h = ((h << 5) - h + s.charCodeAt(i)) | 0;
26 }
27 return h.toString(36);
28}
29
30function check(model?: string): void {
31 const diffStat = getDiffStat();
32 if (!diffStat) {
33 process.exit(0);
34 }
35
36 // Deduplicate: skip if diff unchanged since last check
37 const gitDir = execSync("git rev-parse --git-dir", {
38 encoding: "utf-8",
39 }).trim();
40 const hashFile = join(gitDir, "redline-last-diff");
41 const currentHash = hash(diffStat);
42
43 if (existsSync(hashFile)) {
44 const lastHash = readFileSync(
45 hashFile,
46 "utf-8",
47 ).trim();
48 if (lastHash === currentHash) {
49 process.exit(0);
50 }
51 }
52
53 writeFileSync(hashFile, currentHash);
54
55 const cmd = model
56 ? `redline review --model ${model}`
57 : "redline review";
58
59 const hookOutput = {
60 decision: "block",
61 reason: [
62 "Redline: Here is a summary of uncommitted",
63 "changes since the last review:",
64 "",
65 diffStat,
66 "",
67 "If these changes are substantial enough to",
68 "warrant a code review (e.g., new logic, bug",
69 "fixes, refactors — not just formatting or",
70 "comments), run the following command as a",
71 "background task:",
72 "",
73 ` ${cmd}`,
74 "",
75 "If the changes are trivial, or a review is",
76 "already running, skip it. When a review",
77 "completes, assess the findings and inform",
78 "the user of any issues.",
79 ].join("\n"),
80 };
81
82 console.log(JSON.stringify(hookOutput));
83}

Bước check dùng git diff --stat HEAD để có một bản tóm tắt ngắn gọn về những gì đã thay đổi, và lùi về dùng git status --porcelain cho các file chưa được theo dõi. Nó hash diff stat rồi lưu vào .git/redline-last-diff — nếu diff không đổi so với lần check trước, hook thoát ra một cách lặng lẽ. Nhờ vậy mà cùng một diff không liên tục kích hoạt hook. Diff stat được đưa vào văn bản reason để Claude tự quyết định xem các thay đổi có đáng review hay không.

Xây dựng lệnh review

Lệnh review do Claude spawn như một background task. Nó stream output của Codex theo thời gian thực để hiển thị tiến độ, rồi in ra một bản tóm tắt cuối cùng.

expandable lines
1import { spawn } from "child_process";
2import { readFileSync, unlinkSync } from "fs";
3import { join } from "path";
4import { tmpdir } from "os";
5
6async function review(model?: string): Promise<void> {
7 const outputFile = join(
8 tmpdir(),
9 `redline-review-${Date.now()}.txt`,
10 );
11
12 const args = [
13 "exec",
14 "review",
15 "-c",
16 'model_provider="tram"',
17 "--uncommitted",
18 "-o",
19 outputFile,
20 ];
21
22 if (model) {
23 args.push("-c", `model="${model}"`);
24 }
25
26 // Stream output in real-time so background task
27 // shows progress
28 const exitCode = await new Promise<number>(
29 (resolve) => {
30 const proc = spawn("codex", args, {
31 cwd: process.cwd(),
32 env: process.env,
33 stdio: ["ignore", "inherit", "inherit"],
34 });
35 proc.on("close", (code) => resolve(code ?? 1));
36 },
37 );
38
39 // Read the final review from the -o output file
40 let review = "";
41 try {
42 review = readFileSync(outputFile, "utf-8").trim();
43 unlinkSync(outputFile);
44 } catch {
45 // No output file — output was already streamed
46 }
47
48 if (exitCode !== 0 && !review) {
49 console.error(`Codex review failed (exit ${exitCode}).`);
50 process.exit(1);
51 }
52
53 if (review) {
54 console.log("\n--- Review Summary ---\n");
55 console.log(review);
56 }
57}

Các chi tiết chính:

  • codex exec review --uncommitted — review tất cả
    các thay đổi đã staged, chưa staged và chưa được theo dõi
  • stdio: "inherit" — stream output của Codex theo
    thời gian thực để background task hiển thị tiến độ
  • -o <file> — ghi thông điệp cuối cùng của agent vào một
    file để thu thập output một cách đáng tin cậy
  • -c 'model_provider="tram"' — route
    qua Trạm AI
  • Tùy chọn: -c 'model="~openai/gpt-latest"' để
    override model
  • Exit code được kiểm tra — nếu Codex thất bại và không
    tạo ra output nào, công cụ thoát với một lỗi

Văn bản reason chỉ dẫn Claude

Trường reason của lệnh check chứa diff stat, và để Claude tự quyết định có review hay không:

lines
Redline: Here is a summary of uncommitted changes
since the last review:
src/commands/check.ts | 25 +++++++++++++++------
src/commands/review.ts | 12 +++++-----
2 files changed, 22 insertions(+), 15 deletions(-)
If these changes are substantial enough to warrant
a code review (e.g., new logic, bug fixes, refactors
— not just formatting or comments), run the
following command as a background task:
redline review
If the changes are trivial, or a review is already
running, skip it. When a review completes, assess
the findings and inform the user of any issues.

Claude thấy bản tóm tắt, đánh giá xem các thay đổi có thực chất hay không, rồi hoặc spawn review như một background task, hoặc bỏ qua. Khi bản review hoàn tất, Claude đọc output đã được stream và trình bày các phát hiện.

Cài đặt và gỡ bỏ hook

Công cụ nên có sẵn các lệnh để tự động cài đặt và gỡ bỏ hook khỏi .claude/settings.local.json.

Cài đặt

lines
$redline install

Đọc .claude/settings.local.json, deep-merge mục
Stop hook, rồi ghi lại. Tạo thư mục .claude/
nếu cần. Lệnh này phải có tính idempotent — nếu hook
đã tồn tại với cùng cấu hình thì bỏ qua; nếu nó
tồn tại với một model khác thì cập nhật lại. Hook
được nhận diện bằng tiền tố lệnh (các lệnh bắt đầu bằng
"redline").

File kết quả:

lines
1{
2 "hooks": {
3 "Stop": [
4 {
5 "hooks": [
6 {
7 "type": "command",
8 "command": "redline check",
9 "timeout": 10
10 }
11 ]
12 }
13 ]
14 }
15}

Gỡ bỏ

lines
$redline off

Lọc bỏ các mục hook có command bắt đầu bằng "redline". Dọn dẹp các mảng và đối tượng rỗng (xóa Stop: [] nếu rỗng, xóa hooks: {} nếu rỗng).

Triển khai

expandable lines
1import {
2 readFileSync,
3 writeFileSync,
4 mkdirSync,
5} from "fs";
6import { join } from "path";
7
8const SETTINGS_PATH = join(
9 ".claude",
10 "settings.local.json",
11);
12const HOOK_PREFIX = "redline";
13
14function readSettings(): Record<string, unknown> {
15 try {
16 return JSON.parse(
17 readFileSync(SETTINGS_PATH, "utf-8"),
18 );
19 } catch {
20 return {};
21 }
22}
23
24function writeSettings(
25 settings: Record<string, unknown>,
26): void {
27 mkdirSync(".claude", { recursive: true });
28 writeFileSync(
29 SETTINGS_PATH,
30 JSON.stringify(settings, null, 2) + "\n",
31 );
32}
33
34function installHook(model?: string): void {
35 const settings = readSettings();
36 const command = model
37 ? `${HOOK_PREFIX} check --model ${model}`
38 : `${HOOK_PREFIX} check`;
39
40 const hookEntry = {
41 hooks: [
42 {
43 type: "command",
44 command,
45 timeout: 10,
46 },
47 ],
48 };
49
50 const hooks = (settings.hooks ?? {}) as Record<
51 string,
52 unknown[]
53 >;
54 const stopHooks = (hooks.Stop ?? []) as Array<{
55 hooks: Array<{ command: string }>;
56 }>;
57
58 // Check for existing redline hook
59 const existing = stopHooks.findIndex((h) =>
60 h.hooks?.some((inner) =>
61 inner.command?.startsWith(HOOK_PREFIX),
62 ),
63 );
64
65 if (existing >= 0) {
66 stopHooks[existing] = hookEntry;
67 } else {
68 stopHooks.push(hookEntry);
69 }
70
71 hooks.Stop = stopHooks;
72 settings.hooks = hooks;
73 writeSettings(settings);
74}
75
76function removeHook(): void {
77 const settings = readSettings();
78 const hooks = (settings.hooks ?? {}) as Record<
79 string,
80 unknown[]
81 >;
82 const stopHooks = (hooks.Stop ?? []) as Array<{
83 hooks: Array<{ command: string }>;
84 }>;
85
86 hooks.Stop = stopHooks.filter(
87 (h) =>
88 !h.hooks?.some((inner) =>
89 inner.command?.startsWith(HOOK_PREFIX),
90 ),
91 );
92
93 if (
94 Array.isArray(hooks.Stop) &&
95 hooks.Stop.length === 0
96 ) {
97 delete hooks.Stop;
98 }
99 if (Object.keys(hooks).length === 0) {
100 delete settings.hooks;
101 }
102
103 writeSettings(settings);
104}

Ghép tất cả lại

CLI đầy đủ có bốn lệnh:

lines
$# Install the hook
$redline install
$
$# Install with a specific review model
$redline install --model ~openai/gpt-latest
$
$# Remove the hook
$redline off
$
$# Run a review manually (prints to stdout)
$redline review
$
$# Fast gate check (called by the Stop hook)
$redline check

Điểm vào CLI đầy đủ

expandable lines
1const args = process.argv.slice(2);
2const command = args[0];
3
4const modelFlag = args.indexOf("--model");
5const model =
6 modelFlag >= 0 ? args[modelFlag + 1] : undefined;
7
8switch (command) {
9 case "install":
10 installHook(model);
11 console.log(
12 "Hook installed in",
13 ".claude/settings.local.json",
14 );
15 break;
16
17 case "off":
18 removeHook();
19 console.log("Hook removed.");
20 break;
21
22 case "check":
23 check(model);
24 break;
25
26 case "review":
27 review(model);
28 break;
29
30 default:
31 installHook(model);
32 console.log(
33 "Hook installed in",
34 ".claude/settings.local.json",
35 );
36 break;
37}

Kiểm thử

Xác minh việc cài đặt hook

lines
$redline install
$cat .claude/settings.local.json

Người dùng sẽ thấy mục Stop hook với lệnh
redline check và timeout 10 giây.

Kiểm thử check khi không có thay đổi

Đảm bảo working tree sạch, sau đó:

lines
$redline check

Không có output nào — bước check thoát ra lặng lẽ khi không còn thay đổi nào chưa được commit.

Kiểm thử check khi có thay đổi

Thực hiện một thay đổi nhỏ với bất kỳ file nào, sau đó:

lines
$redline check

Người dùng sẽ thấy JSON có "decision": "block" và một
"reason" chứa bản tóm tắt diff stat và
các chỉ dẫn review.

Kiểm thử review

lines
$redline review

Người dùng sẽ thấy output của Codex được stream, theo sau là một
bản tóm tắt review.

Kiểm thử tích hợp đầy đủ

  1. Cài đặt hook: redline install
  2. Khởi động Claude Code: claude
  3. Yêu cầu Claude thực hiện một thay đổi code nhỏ
  4. Khi Claude hoàn tất, Stop hook kích hoạt bước
    check — nếu có các thay đổi chưa commit, Claude
    spawn review như một background task
  5. Người dùng có thể tiếp tục làm việc trong khi bản review chạy
  6. Khi bản review hoàn tất, Claude đọc output
    và trình bày bất kỳ phát hiện nào

Hạn chế và hướng phát triển

Chỉ dành cho Claude Code

Pattern này dựa vào output hook decision: "block" của Claude Code, vốn đưa các chỉ dẫn thẳng vào cuộc hội thoại của agent. Hệ thống hook của Codex CLI (tính đến đầu năm 2026) còn hạn chế hơn — Stop hook của nó theo kiểu fire-and-forget, không thể đưa structured output trở lại vào context của model. Các native hook ([[hooks]] trong config.toml) hỗ trợ một số sự kiện, nhưng chỉ SessionStart mới đưa được stdout vào model. Không có cơ chế nào tương đương với decision: "block" + reason để chèn chỉ dẫn giữa phiên. Khi Codex bổ sung hỗ trợ đầy đủ cho structured hook output, pattern này có thể được mở rộng thêm.

Mức độ chi tiết của review

Bản review bao quát mọi thứ chưa được commit — tất cả các thay đổi đã staged, chưa staged và chưa được theo dõi. Nó không phân biệt giữa thay đổi Claude vừa thực hiện và công việc chưa commit có sẵn từ trước. Các phiên bản sau này có thể dùng cơ chế phát hiện thay đổi thông minh hơn (ví dụ, so diff với một snapshot cơ sở được chụp trước phản hồi của Claude).

Resources