> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.tramlabs.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.tramlabs.io/_mcp/server.

# Tự động Review Code

> Tự động review code cho Claude Code bằng hook và Trạm AI — các bản review bất đồng bộ, không chặn từ một model thứ hai trong khi bạn vẫn tiếp tục làm việc

> **Info**
>
> Một bản triển khai tham chiếu có sẵn tại\
> [**redline**](https://github.com/alexanderatallah/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](https://tramlabs.io)
> * Đă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`**

```text 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

* Một [Trạm AI API key](https://console.tramlabs.io/keys)
* [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview)\
  đã cài đặt (`claude` có trong PATH)
* [Codex CLI](https://github.com/openai/codex)\
  đã cài đặt (`codex` có trong PATH)
* Runtime [Bun](https://bun.sh/) hoặc Node.js

## 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`**

```bash lines
export ANTHROPIC_BASE_URL="https://api.tramlabs.io"
export ANTHROPIC_AUTH_TOKEN="tram_sk-..."
export ANTHROPIC_API_KEY=""
```

> **Warning**
>
> Base URL là `https://api.tramlabs.io` — **khô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](/tich-hop-coding-agents/claude-code-integration)
đầy đủ để biết chi tiết.

### Codex CLI

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

**`lines`**

```toml lines
[model_providers.tram]
name = "Tram AI"
base_url = "https://api.tramlabs.io/v1"
env_key = "TRAM_AI_API_KEY"
```

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

**`lines`**

```bash lines
export TRAM_AI_API_KEY="tram_sk-..."
```

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

> **Warning**
>
> 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.tramlabs.io/v1` (có
>   `/v1`), trong khi Claude dùng
>   `https://api.tramlabs.io` (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](/tich-hop-coding-agents/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`**

```json lines
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "redline check",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

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`**

```typescript expandable lines
import { execSync } from "child_process";
import {
  existsSync,
  readFileSync,
  writeFileSync,
} from "fs";
import { join } from "path";

function getDiffStat(): string {
  // Prefer diff --stat for a concise summary
  const diff = execSync("git diff --stat HEAD", {
    encoding: "utf-8",
  }).trim();
  if (diff) return diff;

  // Fall back to status for untracked files
  return execSync("git status --porcelain", {
    encoding: "utf-8",
  }).trim();
}

function hash(s: string): string {
  let h = 0;
  for (let i = 0; i < s.length; i++) {
    h = ((h << 5) - h + s.charCodeAt(i)) | 0;
  }
  return h.toString(36);
}

function check(model?: string): void {
  const diffStat = getDiffStat();
  if (!diffStat) {
    process.exit(0);
  }

  // Deduplicate: skip if diff unchanged since last check
  const gitDir = execSync("git rev-parse --git-dir", {
    encoding: "utf-8",
  }).trim();
  const hashFile = join(gitDir, "redline-last-diff");
  const currentHash = hash(diffStat);

  if (existsSync(hashFile)) {
    const lastHash = readFileSync(
      hashFile,
      "utf-8",
    ).trim();
    if (lastHash === currentHash) {
      process.exit(0);
    }
  }

  writeFileSync(hashFile, currentHash);

  const cmd = model
    ? `redline review --model ${model}`
    : "redline review";

  const hookOutput = {
    decision: "block",
    reason: [
      "Redline: Here is a summary of uncommitted",
      "changes since the last review:",
      "",
      diffStat,
      "",
      "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:",
      "",
      `  ${cmd}`,
      "",
      "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.",
    ].join("\n"),
  };

  console.log(JSON.stringify(hookOutput));
}
```

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`**

```typescript expandable lines
import { spawn } from "child_process";
import { readFileSync, unlinkSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";

async function review(model?: string): Promise<void> {
  const outputFile = join(
    tmpdir(),
    `redline-review-${Date.now()}.txt`,
  );

  const args = [
    "exec",
    "review",
    "-c",
    'model_provider="tram"',
    "--uncommitted",
    "-o",
    outputFile,
  ];

  if (model) {
    args.push("-c", `model="${model}"`);
  }

  // Stream output in real-time so background task
  // shows progress
  const exitCode = await new Promise<number>(
    (resolve) => {
      const proc = spawn("codex", args, {
        cwd: process.cwd(),
        env: process.env,
        stdio: ["ignore", "inherit", "inherit"],
      });
      proc.on("close", (code) => resolve(code ?? 1));
    },
  );

  // Read the final review from the -o output file
  let review = "";
  try {
    review = readFileSync(outputFile, "utf-8").trim();
    unlinkSync(outputFile);
  } catch {
    // No output file — output was already streamed
  }

  if (exitCode !== 0 && !review) {
    console.error(`Codex review failed (exit ${exitCode}).`);
    process.exit(1);
  }

  if (review) {
    console.log("\n--- Review Summary ---\n");
    console.log(review);
  }
}
```

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="gpt-5.5"'` để\
  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`**

```text 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`**

```bash 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`**

```json lines
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "redline check",
            "timeout": 10
          }
        ]
      }
    ]
  }
}
```

### Gỡ bỏ

**`lines`**

```bash 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`**

```typescript expandable lines
import {
  readFileSync,
  writeFileSync,
  mkdirSync,
} from "fs";
import { join } from "path";

const SETTINGS_PATH = join(
  ".claude",
  "settings.local.json",
);
const HOOK_PREFIX = "redline";

function readSettings(): Record<string, unknown> {
  try {
    return JSON.parse(
      readFileSync(SETTINGS_PATH, "utf-8"),
    );
  } catch {
    return {};
  }
}

function writeSettings(
  settings: Record<string, unknown>,
): void {
  mkdirSync(".claude", { recursive: true });
  writeFileSync(
    SETTINGS_PATH,
    JSON.stringify(settings, null, 2) + "\n",
  );
}

function installHook(model?: string): void {
  const settings = readSettings();
  const command = model
    ? `${HOOK_PREFIX} check --model ${model}`
    : `${HOOK_PREFIX} check`;

  const hookEntry = {
    hooks: [
      {
        type: "command",
        command,
        timeout: 10,
      },
    ],
  };

  const hooks = (settings.hooks ?? {}) as Record<
    string,
    unknown[]
  >;
  const stopHooks = (hooks.Stop ?? []) as Array<{
    hooks: Array<{ command: string }>;
  }>;

  // Check for existing redline hook
  const existing = stopHooks.findIndex((h) =>
    h.hooks?.some((inner) =>
      inner.command?.startsWith(HOOK_PREFIX),
    ),
  );

  if (existing >= 0) {
    stopHooks[existing] = hookEntry;
  } else {
    stopHooks.push(hookEntry);
  }

  hooks.Stop = stopHooks;
  settings.hooks = hooks;
  writeSettings(settings);
}

function removeHook(): void {
  const settings = readSettings();
  const hooks = (settings.hooks ?? {}) as Record<
    string,
    unknown[]
  >;
  const stopHooks = (hooks.Stop ?? []) as Array<{
    hooks: Array<{ command: string }>;
  }>;

  hooks.Stop = stopHooks.filter(
    (h) =>
      !h.hooks?.some((inner) =>
        inner.command?.startsWith(HOOK_PREFIX),
      ),
  );

  if (
    Array.isArray(hooks.Stop) &&
    hooks.Stop.length === 0
  ) {
    delete hooks.Stop;
  }
  if (Object.keys(hooks).length === 0) {
    delete settings.hooks;
  }

  writeSettings(settings);
}
```

## Ghép tất cả lại

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

**`lines`**

```bash lines
# Install the hook
redline install

# Install with a specific review model
redline install --model gpt-5.5

# 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`**

```typescript expandable lines
const args = process.argv.slice(2);
const command = args[0];

const modelFlag = args.indexOf("--model");
const model =
  modelFlag >= 0 ? args[modelFlag + 1] : undefined;

switch (command) {
  case "install":
    installHook(model);
    console.log(
      "Hook installed in",
      ".claude/settings.local.json",
    );
    break;

  case "off":
    removeHook();
    console.log("Hook removed.");
    break;

  case "check":
    check(model);
    break;

  case "review":
    review(model);
    break;

  default:
    installHook(model);
    console.log(
      "Hook installed in",
      ".claude/settings.local.json",
    );
    break;
}
```

## Kiểm thử

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

**`lines`**

```bash 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`**

```bash 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`**

```bash 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`**

```bash 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

* [Bản triển khai tham chiếu (redline)](https://github.com/alexanderatallah/redline)
* [Tích hợp Claude Code](/tich-hop-coding-agents/claude-code-integration)
* [Tích hợp Codex CLI](/tich-hop-coding-agents/codex-cli)
* [Trạm AI Activity Dashboard](https://console.tramlabs.io/settings/logs)