Chuyển tới nội dung chính

Kiểm thử plugin Claude Code bằng claude plugin eval: tôi dựng thật, chạy thật, và bị chặn giữa chừng

Claude Code 2.1.269 (11/9/2026) thêm claude plugin eval: chạy plugin thật trên model thật, so WITH/W-OUT một no-plugin baseline, và tính Δ. Tôi dựng một plugin thật enforce quy tắc 3800-từ của chính blog này, xác nhận claude --version, claude plugin validate và eval init --bare chạy tốt trên sandbox 2.1.269 — nhưng khi chạy suite thật, bị chính auto-mode classifier của Claude Code chặn lại vì spawn một claude -p con bị xếp vào nhóm Create Unsafe Agents.

Trong bài này

Claude Code 2.1.269, ra ngày 11/9/2026, thêm một lệnh mới: claude plugin eval. Đây không phải một trình kiểm tra cú pháp như claude plugin validate mà tôi đã viết ở bài trước — nó thực sự chạy plugin của bạn trên một mô hình thật, nhiều lần, có và không có plugin, rồi tính điểm chênh lệch. Tôi dựng một plugin thật để kiểm thử plugin Claude Code theo đúng cách tài liệu mô tả, cho nó enforce chính quy tắc biên tập của blog này (đếm đủ 3800 từ trước khi nộp bài), rồi chạy suite ngay trên sandbox 2.1.269 đang gõ bài viết này — và bị chặn giữa chừng bởi một lớp an toàn tôi không lường trước.

Kiểm thử plugin Claude Code bằng claude plugin eval: cơ chế thật đằng sau

Trước 2.1.269, công cụ duy nhất để kiểm tra một plugin Claude Code là claude plugin validate — thứ tôi đã mổ xẻ trong bài về lỗ hổng symlink (claude-code-va-lo-hong-symlink-plugin-trinh-validate-van-cho-qua). Nhưng validate chỉ soi cấu trúc file và schema manifest — nó không nói được gì về việc plugin có thực sự hoạt động đúng không, tức là liệu Claude có chọn đúng skill khi người dùng gõ một câu tự nhiên, và liệu kết quả cuối cùng có đúng như kỳ vọng.

Tài liệu chính thức tại code.claude.com/docs/en/plugin-evals mô tả claude plugin eval như sau, tôi trích nguyên văn:

claude plugin eval runs your plugin against a suite of test cases and scores the results. Each case is a realistic prompt plus one or more graders. A grader is a pass/fail check on what Claude produced, such as a regex over the reply, whether a particular tool was called, or a rubric that a second model judges the reply against.

Nói cách khác: mỗi "case" là một prompt giống hệt một người dùng thật sẽ gõ, và một hoặc nhiều "grader" — bộ chấm điểm — để xác nhận Claude phản ứng đúng. Tài liệu liệt kê chính xác 6 loại grader, chia làm hai nhóm theo chi phí:

LoạiMiễn phí hay tốn tiềnĐiều kiện PASS
regexMiễn phí (tính từ transcript/file)Regex JavaScript khớp trong target
tool_usedMiễn phíSố lần gọi tool (khớp input_match nếu có) nằm trong khoảng min–max
tool_orderMiễn phíCả hai tool đều được gọi, và lần gọi khớp before đứng trước lần gọi khớp after
file_existsMiễn phíMột file Claude tạo ra khớp glob path (hoặc không file nào khớp, nếu exists: false)
llmTốn tiền (gọi judge model)Judge model vote PASS ở ít nhất 2/3 lần bỏ phiếu, theo rubric trong criteria
baselineTốn tiềnJudge cho rằng kết quả đạt yêu cầu tối thiểu bằng transcript tham chiếu tại baseline_file

Không có grader chạy code tuỳ ý — tài liệu nói thẳng: "There are no custom-code graders." Đây là một lựa chọn thiết kế có chủ đích: mọi phép chấm điểm đều nằm trong 6 khuôn có sẵn, để suite luôn tái lập được và không thể lén nhét mã tuỳ tiện vào bước chấm.

Phần thú vị nhất, theo tôi, là cơ chế no-plugin baseline. Mỗi case mặc định chạy hai "arm": with-arm (có plugin) và without-arm (không có plugin nào cả), mỗi arm chạy 3 lần theo mặc định (runs: 3). Điểm chênh lệch, ký hiệu Δ, mới là con số thật sự đáng tin — vì như tài liệu giải thích:

A high score on its own doesn't tell you the plugin helped, because Claude might do as well without it. [...] If a case scores 1.0 both with and without the plugin, the plugin isn't what made it pass.

Cơ chế này giải quyết đúng một sai lầm phổ biến khi test agent: một prompt "dễ" mà Claude vốn đã trả lời tốt ngay cả khi không có skill nào, sẽ luôn cho điểm WITH cao — nhưng đó không chứng minh được gì về plugin. Δ gần 0 kèm theo grader tool_used: Skill fail là dấu hiệu kinh điển: skill không được kích hoạt đúng lúc, vấn đề nằm ở trường description trong SKILL.md chứ không phải ở logic bên trong.

Một chi tiết dễ bị đọc lướt qua nhưng quan trọng khi thiết kế grader: tool_used với tool: Skill mặc định không được tính điểm trong chế độ hai-arm, mà chỉ hiển thị như một "plugin-fired indicator". Lý do rất hợp lý — một check kiểu "skill đã được gọi" thì không đời nào pass được ở without-arm (vì làm gì có plugin nào mà gọi), nên nếu tính điểm bình thường sẽ kéo điểm without-arm về 0 một cách giả tạo và làm phồng Δ lên. Muốn ép loại grader này tính điểm ở cả hai arm (ví dụ để khẳng định "skill không được gọi" là điều đúng), phải gắn arm: both — tôi sẽ dùng đúng cờ này bên dưới.

Chi phí cũng được tài liệu tính rõ thành công thức: một suite tốn khoảng cases × runs lần chạy agent có plugin, cộng thêm ngần ấy cho baseline, cộng ba lần gọi judge ngắn cho mỗi grader llm hoặc baseline mỗi lần chạy. Không có grader nào cần judge trong bản tôi dựng, nên chi phí chỉ còn là số lần chạy agent thuần.

Dành cho CI, lệnh có --threshold (mặc định 1.0, tức phải pass tuyệt đối), --max-cost-usd để chặn chi phí, và --trust-plugin để bỏ qua bước hỏi "Trust this plugin directory?" khi chạy không có terminal. Mã thoát (exit code) được tài liệu định nghĩa rõ: 0 mọi case đạt threshold, 1 có case dưới threshold hoặc lỗi cấu hình, 2 chạy dở dang vì chạm --max-cost-usd hoặc credential bị từ chối, 130 bị ngắt (interrupted), 143 bị terminate (ví dụ do CI timeout).

Tôi dựng thật một plugin: enforce đúng luật biên tập của blog này

Thay vì viết một plugin demo vô thưởng vô phạt, tôi chọn cái gì đó có anchor thật: quy tắc biên tập chính bài viết này đang phải tuân theo — "tối thiểu 3800 từ tiếng Việt (đếm theo khoảng trắng)... đếm bằng wc -w trước khi nộp." Tôi build một plugin tên blog-length-guard, chỉ một skill duy nhất: kiem-do-dai, việc của nó là nhận một đoạn nháp, đếm số từ theo khoảng trắng, ghi ra file report.md với format cố định, rồi báo PASS/FAIL so với ngưỡng 3800.

Cấu trúc plugin, theo đúng layout evals/ mà tài liệu mô tả (<case>/prompt.md + <case>/graders/*.md):

blog-length-guard/
├── .claude-plugin/plugin.json
├── skills/kiem-do-dai/SKILL.md
└── evals/
    ├── kiem-do-dai-truoc-khi-nop/
    │   ├── prompt.md
    │   └── graders/
    │       ├── skill-fired.md
    │       ├── report-written.md
    │       └── report-correct.md
    └── dich-cau-khong-lien-quan/
        ├── prompt.md
        └── graders/skill-not-fired.md

plugin.json:

json
{
  "name": "blog-length-guard",
  "version": "0.1.0",
  "description": "Đếm số từ một bản nháp tiếng Việt và xác nhận đạt ngưỡng tối thiểu trước khi nộp lên CMS."
}

skills/kiem-do-dai/SKILL.md, phần frontmatter — chỗ quyết định Claude có chọn đúng skill này hay không, theo mô tả (description) chứ không phải theo tên:

yaml
---
name: kiem-do-dai
description: Đếm số từ (tách theo khoảng trắng) của một bản nháp bài viết tiếng
  Việt và xác nhận đã đạt ngưỡng tối thiểu 3800 từ trước khi nộp lên CMS chưa.
  Dùng khi người dùng dán một đoạn nháp và hỏi bài đã đủ dài để nộp chưa, nhờ
  đếm từ, hoặc nhờ kiểm tra độ dài trước khi submit.
---

Case đầu tiên, kiem-do-dai-truoc-khi-nop/prompt.md, viết như một người dùng thật sẽ gõ — không nhắc tên skill, đúng nguyên tắc tài liệu khuyên ("phrased the way a user would type it rather than naming the skill"):

markdown
---
max_turns: 6
allowed_tools: [Read, Write, Skill]
---

Đây là đoạn mở đầu bản nháp mình vừa viết, sắp nộp lên CMS rồi: "Next.js
robots.txt bị chặn ngay trên chính sản phẩm của mình khiến tôi phải đối chiếu
lại với tài liệu chính thức về execution order của Proxy..." Bạn xem giúp bài
này đã đủ dài để nộp chưa, ghi kết quả ra file report.md nhé.

Ba grader cho case này. Grader đầu xác nhận skill có được gọi không (theo đúng cú pháp input_match mẫu trong tài liệu, khớp cả dạng namespaced plugin-name:skill-name):

yaml
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?kiem-do-dai"'
---

Grader thứ hai xác nhận file report.md thực sự được tạo ra:

yaml
---
type: file_exists
path: report.md
---

Grader thứ ba — chỗ tôi thấy tài liệu thiết kế khéo nhất — chấm điểm trên nội dung file, không phải trên câu trả lời cuối cùng của Claude, bằng cách trỏ target vào { source: file, path: ... }:

yaml
---
type: regex
target:
  source: file
  path: report.md
pattern: 'KET_QUA:\s*FAIL'
---

Đoạn nháp tôi đưa vào prompt chỉ khoảng 40 từ, chắc chắn dưới 3800, nên report.md đúng phải chứa KET_QUA: FAIL.

Case thứ hai để bắt trường hợp ngược: một yêu cầu chẳng liên quan gì đến đếm từ ("Dịch giúp mình câu này sang tiếng Anh..."), skill không được phép kích hoạt. Đây là chỗ tôi cần arm: both, vì mặc định tool_used: Skill bị loại khỏi điểm số ở chế độ hai-arm:

yaml
---
type: tool_used
tool: Skill
input_match: '"skill"\s*:\s*"(?:[\w-]+:)?kiem-do-dai"'
min: 0
max: 0
arm: both
---

Hai case này đủ để đo đúng thứ tài liệu gọi là "the plugin's own contribution": case 1 đo xem plugin có khiến Claude đếm từ và ghi report đúng cách chuẩn hoá (không phải Claude tự bịa ra format tương tự), case 2 đo xem plugin có tránh kích hoạt nhầm trên yêu cầu không liên quan hay không — cả hai đều là lỗi thật mà một skill viết ẩu có thể mắc.

Plugin blog-length-guard của tôi không cần gọi MCP server nào, nhưng nếu sau này tôi muốn viết eval suite cho một skill thật gắn với MCP server có OAuth mà chính pipeline này đang dùng để nộp bản nháp lên CMS, tài liệu có một cơ chế riêng cho đúng tình huống đó: mock MCP server. Thay vì để case gọi thẳng vào create_draft hay list_posts thật — rủi ro tạo rác hoặc đụng dữ liệu production — bạn đặt một file Markdown mỗi tool dưới evals/mocks/<server>/<tool>.md, và theo tài liệu:

A run never starts your plugin's real MCP servers unless you ask. Claude Code registers a stand-in under each server's own name. Tools with a mock file answer from it [...] and a tool with no mock file isn't available to Claude.

Phần thân file mock chính là kết quả trả về, cho phép chèn field từ input bằng {{input.<field>}}, và một khối expect: để chặn cứng — nếu Claude gọi tool với input sai định dạng, cả lần chạy dừng ngay với điểm 0 và lý do được ghi lại. Đây đúng là kiểu ràng buộc tôi muốn có nếu viết eval cho một skill có quyền gọi create_draft thật: buộc nó phải gửi đúng field title, content, tags theo schema, mà không cần một bản nháp thật nào bị tạo ra trong lúc test. Tôi không dựng phần này trong suite thử nghiệm ở đây vì nó không cần thiết cho plugin kiem-do-dai, nhưng đáng ghi lại vì đây là mảnh ghép sẽ cần đến ngay khi eval suite chạm tới bất cứ MCP server thật nào.

Test tĩnh trước: validate và eval init --bare chạy tốt

Trước khi chạm tới phần chạy hành vi thật (tốn tiền, tốn thời gian, và — như sẽ thấy — vướng một lớp an toàn), tôi xác nhận version và cấu trúc file bằng hai lệnh tĩnh, không sinh phiên agent con nào.

Trước hết, chính sandbox đang gõ bài này chạy đúng bản vừa thêm tính năng:

terminal
$ claude --version
2.1.269 (Claude Code)

Tài liệu ghi yêu cầu tối thiểu "Claude Code v2.1.269 or later" — sandbox này vừa đúng bằng ngưỡng, không hơn không kém.

claude plugin validate . chạy trên blog-length-guard, output thật:

terminal
$ claude plugin validate .
Validating plugin manifest: .../blog-length-guard/.claude-plugin/plugin.json

‼ Found 1 warning:

  > author: No author information provided. Consider adding author details for plugin attribution

√ Validation passed with warnings

Không có lỗi cấu trúc nào — manifest, skill, và cây evals/ tôi viết tay đều hợp lệ, chỉ thiếu trường author không bắt buộc.

Tiếp theo, tôi thử claude plugin eval init --bare, lệnh viết ra một case mẫu trống mà — theo tài liệu — "runs nothing", tức không cần gọi model nào:

terminal
$ claude plugin eval init --bare demo-case
Created evals/demo-case/prompt.md and evals/demo-case/graders/criteria.md

Lệnh chạy thành công, exit code 0, và hai file được tạo đúng như mô tả. Đọc lại nội dung thật của graders/criteria.md mà lệnh sinh ra, tôi thấy một chi tiết tài liệu không nhắc tới trong đoạn ví dụ quickstart: file có kèm weight: 1 trong frontmatter, chứ không chỉ type: llm trơn như snippet minh hoạ trong "Write a case manually":

yaml
---
type: llm
weight: 1
---

TODO: describe what a successful response looks like

Một khác biệt nhỏ, vô hại, nhưng là ví dụ cho việc tài liệu và hành vi thật của binary không phải lúc nào cũng khớp từng ký tự — lý do tôi luôn chạy thử thay vì chỉ đọc doc.

Cả hai lệnh tĩnh này đều không đụng tới phần lõi của plugin eval: chạy plugin trên một phiên agent thật. Đó là bước tiếp theo, và cũng là bước mọi chuyện dừng lại.

Chạy thật: bị chính lớp containment của Claude Code chặn

Tôi gọi lệnh chạy suite thật, chỉ một case, một lần chạy, tắt baseline để giữ chi phí thấp nhất có thể cho một lần smoke-test:

terminal
$ claude plugin eval . --trust-plugin --allow-tools Write \
    --runs 1 --ablation none --case kiem-do-dai-truoc-khi-nop

Lệnh không chạy được. Kết quả trả về nguyên văn:

Permission for this action was denied by the Claude Code auto mode classifier. Reason: [Create Unsafe Agents]. If you have other tasks that don't depend on this action, continue working on those. IMPORTANT: You may attempt to accomplish this action using other tools that might naturally be used to accomplish this goal [...] You should not attempt to work around this denial in malicious ways [...] Get as much of the rest of the task done as you can, then STOP and explain to the user what you were trying to do and why you need this permission.

Để chắc chắn đây không phải lỗi cú pháp lệnh của tôi, tôi thử claude plugin eval --help (không thực thi gì) — chạy bình thường, in đủ danh sách flag khớp với tài liệu, kể cả một câu tôi thấy đáng chú ý mà trang docs không có nguyên văn: --trust-plugin được --help mô tả là "like --dangerously-skip-permissions, only pass it for plugins you would run yourself." Vậy bản thân binary cũng tự xếp --trust-plugin vào cùng nhóm rủi ro với cờ bỏ qua toàn bộ permission — một lời cảnh báo rõ hơn cả tài liệu web.

Vậy vấn đề không nằm ở cú pháp, mà ở đúng hành vi lõi của lệnh: chạy suite nghĩa là spawn một phiên claude -p con, cô lập, dùng credential của tôi. Tài liệu plugin-evals mô tả chính xác điều này ở mục "How runs are isolated":

Each run gets a throwaway home directory, working directory, and Claude Code configuration, and the agent under test runs there as a claude -p child process with only your plugin loaded.

Và đây chính là hành vi mà lớp containment của phiên tôi đang chạy — không phải của bản thân claude plugin eval, mà của outer session đang thực thi lệnh Bash này — coi là "Create Unsafe Agents": tạo ra một agent con mới, có credential, chạy độc lập. Tôi tìm tài liệu chính thức về cơ chế này tại code.claude.com/docs/en/auto-mode-config, trang mô tả "auto mode" — chế độ Claude Code chạy không cần hỏi quyền cho từng hành động, thay vào đó định tuyến mọi tool call qua một bộ phân loại (classifier):

Auto mode lets Claude Code run without routine permission prompts by routing tool calls through a classifier that blocks anything irreversible, destructive, or aimed outside your environment.

Trang này liệt kê rõ cơ chế phản hồi khi bị chặn, và một chi tiết quan trọng để hiểu đúng thông điệp tôi nhận được:

The reason shown with the call is the fixed text Blocked by classifier in most sessions [...] Some sessions run a classifier model that writes a short explanation instead [...] when one appears, treat it as a hint about which destination or intent the classifier was missing.

Nói cách khác: chuỗi [Create Unsafe Agents] tôi nhận được không phải một mã lỗi cố định, được tài liệu hoá đầy đủ ở đâu đó, mà là một lời giải thích do chính classifier tự viết ra cho trường hợp cụ thể này — tài liệu gọi đây là hành vi của "some sessions" chạy một classifier model sinh giải thích, khác với đa số phiên chỉ trả về text cố định "Blocked by classifier". Tôi cũng đối chiếu với bài viết kỹ thuật của Anthropic mô tả thiết kế classifier này (anthropic.com/engineering/claude-code-auto-mode), nơi liệt kê 4 nhóm hành vi mặc định bị chặn — "Destroy or exfiltrate", "Degrade security", "Cross trust boundaries", "Bypass review/affect others" — nhưng không có nhóm nào tên chính xác là "Create Unsafe Agents". Bài viết đó có nhắc tới việc classifier "runs at both delegation and return points for subagent handoffs" khi xử lý sub-agent, nên nhiều khả năng việc spawn một claude -p con độc lập với credential đầy đủ (chứ không phải một sub-agent nội bộ qua tool Agent) rơi vào một trong các luật "Cross trust boundaries" hoặc một luật riêng không nằm trong 4 nhóm được tóm tắt công khai — nhưng tôi không có cách xác nhận chính xác luật nào trong danh sách hơn 20 quy tắc soft_deny/hard_deny đã kích hoạt, vì claude auto-mode config (lệnh in ra bộ luật hiệu lực) đòi hỏi đúng quyền tôi vừa bị từ chối để kiểm tra bằng thực nghiệm mà không tự suy diễn thêm.

Điều tôi có thể xác nhận chắc chắn, không suy đoán: lệnh claude plugin eval chưa từng chạy đến bước gọi model trong lần thử này. Việc chặn xảy ra trước khi tiến trình con được tạo ra — tức là ở đúng lớp containment bọc ngoài phiên tôi đang thao tác, không phải một lỗi hay giới hạn của bản thân tính năng plugin eval.

Đây không phải lần đầu, và không phải một lỗi cần vá

Theo kiểm kê ở bước đầu tiên trước khi viết bài này, tôi thấy trong kho bản nháp của chính blog đã có một bài khác (chưa xuất bản) ghi lại đúng một hiện tượng họ hàng: nỗ lực lồng một phiên MCP server con bên trong sandbox này từng bị "rule Containment Escape" của Claude Code chặn lại. Tôi không thể trích dẫn công khai bài đó vì nó chưa lên live, nhưng nó cho thấy đây là một mẫu hình lặp lại, không phải sự cố ngẫu nhiên riêng của lần thử plugin eval này: bất cứ hành động nào từ bên trong sandbox cố gắng tạo ra một tiến trình agent độc lập khác — dù là một MCP server con, hay một claude -p con để chạy eval — đều chạm vào cùng một loại ranh giới containment.

Điều này hợp lý về mặt thiết kế bảo mật hơn là một khiếm khuyết. Tài liệu auto-mode-config mô tả rõ triết lý: một agent tự động không nên có quyền tự nhân bản mình thành một tiến trình con mang credential đầy đủ mà không có sự giám sát nào — vì đó chính xác là hình dạng của một kịch bản leo thang quyền hoặc thoát khỏi containment nếu agent bị chiếm quyền hay bị dẫn dắt sai. claude plugin eval — đúng như cảnh báo ở mục "What a run can access" trong tài liệu plugin-evals — "loads the target plugin's skills and hooks and runs its eval suite on your machine, as you," và nói thẳng: "Pointing it at a plugin is the same trust decision as claude --plugin-dir, so only evaluate plugins you trust." Một tính năng được thiết kế để chạy mã của người khác (skill, hook, thậm chí MCP server thật) dưới danh nghĩa của bạn, dùng credential của bạn, tất yếu là loại hành động mà một lớp containment nghiêm ngặt phải để mắt tới đầu tiên.

Nhìn từ góc độ ai đang vận hành một pipeline nộp bài tự động như blog này — nơi một scheduled task chạy Claude Code, gọi MCP server có OAuth để tạo bản nháp — thì việc chính hạ tầng agent framework chặn hành vi "tạo agent con" ngay trong pipeline đó là một tín hiệu tốt, không phải xấu: nó có nghĩa là dù một skill hay một MCP tool nào đó trong chuỗi có bị lừa để cố mở một tiến trình con ngoài kiểm soát, containment vẫn đứng chắn ở đó trước khi credential bị nhân bản ra một context mới.

Vậy dùng claude plugin eval ở đâu thì được?

Kết luận thực dụng cho ai muốn viết eval suite cho plugin hay skill của mình: claude plugin eval được thiết kế để chạy ở máy phát triển của bạn (nơi bạn đã tự trust thư mục, tự cấp --allow-tools, tự chịu trách nhiệm) hoặc trong một CI runner riêng, có credential riêng (ANTHROPIC_API_KEY), không nằm lồng bên trong một phiên Claude Code khác đang tự nó chạy dưới auto mode nghiêm ngặt. Chính đoạn hướng dẫn CI trong tài liệu cũng ngầm giả định điều này — job CI chạy claude plugin eval như một step độc lập, không phải như một tool call phát ra từ bên trong một agent session khác:

bash
claude plugin eval . \
  --trust-plugin \
  --json results.json \
  --threshold 0.8 \
  --model claude-sonnet-5 \
  --judge-model claude-haiku-4-5 \
  --no-publish \
  --max-cost-usd 20

Nếu trong tương lai tôi muốn viết skill riêng cho pipeline biên tập của beru.io.vn — ví dụ chính cái kiem-do-dai tôi vừa dựng, biến từ một plugin demo thành một skill thật gắn vào quy trình nộp bài — bài học rút ra không phải là "tính năng này hỏng", mà là: viết và chạy eval suite trên máy cá nhân hoặc một CI job tách biệt, publish report ở đó, rồi mới đưa skill đã qua kiểm thử vào pipeline sản xuất chạy dưới containment nghiêm ngặt — chứ không cố gọi claude plugin eval từ ngay bên trong chính pipeline production đang chạy dưới auto mode.

Một góc nhìn khác đáng nói thêm: cơ chế Δ (delta) so với no-plugin baseline mà claude plugin eval cung cấp giải quyết đúng câu hỏi mà claude plugin validate không bao giờ trả lời được — liệu một skill có thực sự cần thiết hay chỉ là một lớp bọc không đóng góp gì. Với hệ sinh thái plugin Claude Code đang phình to (chính blog này từng đo được 89 skill thừa trong một sandbox khác ở bài về /skill-doctor), có thêm công cụ đo "plugin này có thực sự làm được việc nó hứa không" — thay vì chỉ đo "plugin này có đúng cú pháp không" — là một bước tiến đáng để bất cứ ai viết skill cho Claude Code thử qua, miễn là chạy nó đúng chỗ.

Ai muốn xem toàn bộ 18 project thật mà stack Next.js/React/Vue/Nuxt/Laravel này đã áp dụng, kể cả những project có gắn AI, có thể xem ở trang dịch vụ hoặc đọc case study Kiwi — sản phẩm nợ-chia-tiền tôi tự vận hành, nơi tôi sẽ là người đầu tiên thử gắn một skill có eval suite thật vào quy trình phát triển.

Điều tôi chưa kiểm chứng được

  • Tôi chưa thể chạy trọn vẹn bất kỳ claude plugin eval case nào end-to-end trên sandbox này, vì bị chặn ở bước spawn tiến trình con. Toàn bộ mô tả về format report, bảng tóm tắt WITH/W-OUT/Δ, và cấu trúc aggregate-result.json trong bài đều lấy nguyên văn từ tài liệu chính thức, không phải output tôi tự tạo ra.
  • Tôi không xác nhận được luật soft_deny/hard_deny cụ thể nào trong bộ hơn 20 luật auto-mode đã kích hoạt cho trường hợp này, vì lệnh claude auto-mode config (để in bộ luật hiệu lực) đòi đúng loại quyền tôi vừa bị từ chối.
  • Nhãn [Create Unsafe Agents] là giải thích do classifier tự sinh cho phiên này, theo đúng cơ chế tài liệu mô tả ("some sessions run a classifier model that writes a short explanation") — không phải một mã lỗi cố định, có tài liệu liệt kê đầy đủ ở nơi công khai nào tôi tìm thấy.
  • Tôi không khẳng định gì về cấu hình hạ tầng cụ thể của beru.io.vn (số tiến trình, có cluster hay không, phiên bản Claude Code đang chạy cho các tác vụ khác) — chỉ xác nhận được đúng bản claude --version của phiên đang gõ bài viết này tại thời điểm viết.

Viết và kiểm chứng bởi Nguyễn Hoàng Lộc (Beru), TP. Hồ Chí Minh