Tôi viết bài này khi đang ngồi trong một background session của Claude Code. Worktree branch worktree/agent-ad07a..., dispatch từ một parent session đang theo dõi 12 agent song song khác. Không phải lý thuyết, là dogfooding. Bài 1 (anatomy của async coding) đã đặt nền cho khái niệm “agent chạy nền, làm việc trong sandbox riêng, push PR khi xong”. Phần này: Claude Code (CC) làm việc đó như thế nào trên máy bạn, không phải trên service cloud xa lạ.
Quan điểm cá nhân trước khi đi sâu: với repo cá nhân của tôi, CC BG là default. Code không rời máy, không tốn credit cloud, daemon trên MacBook hoặc homelab giữ session sống xuyên qua disconnect mạng. Cursor BG tôi để dành cho khi đang đi ra ngoài không có laptop tốt. Devin tôi gần như không động vào cho project cá nhân. Chi tiết về Cursor và Devin để bài 3-4, ở đây ta nói về CC.
Khi nào tôi mở /bg
Sync session Claude Code trong terminal giữ chân terminal đó. Tôi không thể đóng iTerm tab, không thể reboot máy, không thể đi cà phê quá lâu vì WiFi sleep sẽ làm session timeout. BG mode đảo ngược bài toán. Session vẫn chạy nhưng không cần terminal nào attach. Đóng iTerm tab, reboot WiFi router, đi ngủ, daemon CC giữ process sống. Khi cần xem progress, gõ claude agents ở bất kỳ terminal nào, FleetView hiện list.
Bốn tình huống hàng tuần đẩy tôi vào BG mode. Thứ nhất là batch nhiều task song song, ví dụ chính bài này, parent session spawn 12 agent với mỗi agent một bài blog và một worktree riêng. Thay vì viết tuần tự 12 bài trong 12 ngày, agent batch xong trong khoảng một giờ. Thứ hai là long refactor không cần babysit, kiểu đổi tên một symbol qua 200 file, chạy codemod, fix edge case không match regex; sync mode tôi sẽ phải ngồi nhìn 30 phút, BG mode tôi tab sang Slack và để session tự xong. Thứ ba là test/build chạy nền song song với edit foreground, tôi edit code ở main session trong khi BG session chạy npm test --watch plus phân tích failure plus đề xuất fix. Thứ tư là remote work qua mạng chập chờn, kiểu SSH từ iPad qua Termius vào homelab Ubuntu, café WiFi chập chờn; sync interactive mỗi lần disconnect là mất terminal state, BG session ở phía homelab vẫn sống nhờ daemon.
/bg, daemon, FleetView, ba khái niệm cần phân biệt
Cách phổ biến nhất để vào BG mode là gõ /bg trong một session sync đang chạy. Session detach khỏi terminal, daemon adopt nó, terminal về shell prompt bình thường. Có hai cách khác: claude --bg "<prompt>" từ shell start ngay một session BG fresh, hữu ích cho automation hoặc fire-and-forget từ script; dispatch từ FleetView (claude agents, gõ prompt vào input bar) spawn session mới với source “fleet” tự động BG. Ba cách, cùng kết quả: session sống trong daemon, không gắn terminal, xem được trong FleetView.
Sự khác biệt sync vs BG gói gọn ở một câu: ai giữ stdio. Sync mode terminal giữ, BG mode daemon giữ. Tool loop, context window, model invocation đều y hệt. Đừng nghĩ BG mode là một “mode khác” về capability, nó chỉ là vấn đề transport.
claude agents mở FleetView, một TUI hiển thị toàn bộ Claude Code session đang sống trên máy. Bốn cột: tên session (hoặc ID), trạng thái (working, idle, blocked, completed), nguồn (cli, fleet, slash, bg), và cwd. Phím tắt thường dùng: Enter dispatch session mới, / focus search, q quit FleetView nhưng không kill session, k kill session đang focus. Tôi đã viết một bài riêng về FleetView (FleetView, một màn hình thay cho 6 tmux pane) nói chi tiết. Ở bài này tôi chỉ cần bạn biết: FleetView là control panel cho BG agent batch.
Khi parent của tôi spawn 12 agent BG, FleetView trông như sau:
NAME STATUS SOURCE CWD
agent-001-non-tech-a Working fleet .claude/worktrees/agent-aa...
agent-002-non-tech-b Working fleet .claude/worktrees/agent-bb...
agent-003-non-tech-c Idle fleet .claude/worktrees/agent-cc...
agent-004-mcp-bai-4 Working fleet .claude/worktrees/agent-dd...
... (12 dòng)
parent-session Idle cli ~/WORK/HENIA/blog-heniart
Parent ở dưới cùng (cwd là repo gốc), 12 agent ở trên (mỗi agent cwd là worktree riêng). Một mắt biết được agent nào busy, agent nào đã done, có agent nào stuck. Source fleet quan trọng vì nó cho biết session dispatch từ FleetView hoặc từ Agent tool call (cùng nhánh code bên trong CC). Source cli là session foreground gõ claude trong terminal, source bg là session spawn bằng claude --bg ..., source slash là dispatch từ RemoteTrigger hoặc Telegram bridge.
EnterWorktree, vì sao BG session cần isolation
Đây là chỗ BG mode khác sync mode về mặt cơ chế, không phải về transport.
Sync mode bạn ngồi trong cwd của mình và edit file của repo trực tiếp. Mở hai sync session cùng repo trong hai terminal, hai session dùng chung working tree, last writer wins. Đôi khi OK, đôi khi đau đầu. BG mode đẩy vấn đề lên thêm bậc: daemon giữ session, bạn không thấy nó đang chạy gì ở foreground. Nếu BG session edit working tree của bạn trong khi bạn cũng đang edit ở main session, race condition không tha cho ai.
CC giải bằng cơ chế mặc định: trước khi BG session bắt đầu dùng tool, nó phải gọi EnterWorktree. Tool này tạo một git worktree riêng tại <git-root>/.claude/worktrees/<slug>/, branch riêng worktree/<slug>, và switch cwd của session vào worktree đó. Mọi edit từ đây xảy ra trong worktree, không động chạm working tree gốc. Harness in ra rõ ràng khi BG session khởi động:
[harness] Background session detected without worktree.
[harness] Calling EnterWorktree tool...
[harness] Worktree created: .claude/worktrees/agent-ad07a..../
[harness] Session cwd updated. Tool loop ready.
Hai feature liên quan nhưng khác nhau, hay nhầm:
| Feature | Áp dụng cho | Khi nào xảy ra |
|---|---|---|
isolation: "worktree" | Subagent spawn từ session khác | Lúc spawn subagent qua Agent tool |
EnterWorktree tool | Background session chính nó | Lúc BG session bắt đầu dùng tool |
isolation: "worktree" là param parent truyền khi gọi Agent tool. EnterWorktree là tool BG session tự gọi cho chính nó. Một bên parent quyết, một bên session tự quyết. Phân biệt rõ vì tôi đã thấy nhiều người confuse hai cái này, và mỗi cái có pitfall riêng. Bài worktree.bgIsolation setting tôi viết riêng phần đó chi tiết hơn.
worktree.bgIsolation và khi nào tôi tắt
Từ CC 2.1.143, có setting worktree.bgIsolation cho phép tắt cơ chế EnterWorktree mặc định:
{
"worktree": {
"bgIsolation": "none"
}
}
Set "none", BG session bỏ qua bước EnterWorktree và edit working tree gốc trực tiếp. Mặc định (không set hoặc set giá trị khác) BG session vẫn vào worktree riêng.
Tôi tắt isolation trong ba tình huống cụ thể. Khi repo có submodule lớn, worktree mới phải init lại submodule và nếu submodule vài GB, thời gian init đôi khi dài hơn cả task BG; tắt isolation để share submodule với main session. Khi build cache theo path tuyệt đối (Bazel, CMake, Rust target), path mới đồng nghĩa cache miss và rebuild từ đầu, một task format code không đáng để rebuild 30 phút dependency. Khi task nhỏ và reversible kiểu update CHANGELOG.md hoặc rename một symbol, overhead worktree không cần, edit thẳng working tree và nếu sai thì git checkout undo.
Ngược lại, isolation ON là default đáng tin cho parallel BG batch (12 agent edit 12 file khác nhau trong cùng repo, share working tree là công thức conflict), cho lúc BG session chạy đồng thời với edit foreground (race condition không track được), và cho risky refactor (rollback dễ qua git worktree remove .claude/worktrees/<slug>, không cần git reset đau đầu).
Setting đặt được ở ba mức: global ~/.claude/settings.json, project <repo>/.claude/settings.json, hoặc local per-machine <repo>/.claude/settings.local.json. Tôi để global default ON, override "none" local cho repo nào có submodule lớn.
Hands-on dispatch 12 agent
Workflow cho batch bài blog tuần này, nguyên văn. Parent session viết plan ra .claude/jobs/<job-id>/batch-plan.md với mỗi agent có pre-allocated file path, frontmatter, outline. Pre-allocate quan trọng để tránh hai agent cùng pick filename trùng, một bài học tôi học sau khi 8 agent của tôi cùng pick seriesOrder: 9 vì cùng đọc memory “số rảnh kế tiếp”. Parent spawn 12 Agent tool call song song, mỗi call một section của plan với isolation: "worktree" và run_in_background: true. Mỗi agent tự EnterWorktree, tự checkout worktree/agent-<short> từ HEAD của parent. Parent ngồi không. FleetView list 13 dòng, tôi tab sang Slack đọc tin nhắn. Mỗi agent xong việc tự git add, git commit, git push -u origin worktree/<slug>, gh pr create --base main. Notification về parent qua Stop hook, Telegram bot tôi cài cũng push qua phone. Parent verify từng PR (lint, em-dash grep, content check), merge tuần tự, pull về main session.
Toàn bộ chu trình từ spawn đến merge xong 12 PR khoảng 90 phút. Sync mode tôi sẽ mất 12 ngày, mỗi ngày một bài, vì tôi không type được 12 bài cùng lúc.
Chi tiết kỹ thuật ít người chú ý: vì isolation: "worktree" base trên parent HEAD (qua WorktreeCreate hook tôi đã cài, xem bài worktree isolation), agent thấy đúng trạng thái uncommitted của parent, không phải origin/main cũ. Quan trọng khi parent có 5 commit chưa push mà agent cần thấy. Đây cũng là chỗ tôi đã gặp incident trước khi sửa hook: agent base trên origin/main outdated, merge ngược overwrite code tôi vừa commit. Hook fix incident đó thành quá khứ.
Cost và resource
BG mode không miễn phí, dù trông giống vậy. Token cost giống sync mode hoàn toàn, daemon không tiết kiệm token; một BG session dùng cùng model với một sync session sẽ tiêu cùng token cho cùng task. Tôi chỉ tiết kiệm thời gian của mình, không tiết kiệm API budget. Daemon process cũng consume RAM, mỗi BG session sống tốn 100-300 MB (Node process plus context buffer), 12 agent batch ăn khoảng 2-3 GB; MacBook M3 Max 32 GB của tôi không vấn đề, nhưng Ubuntu homelab 16 GB phải cẩn thận đặc biệt khi chạy Docker stack song song. Disk cho worktree cũng đáng kể, mỗi worktree share object store nhưng working files duplicate; repo 100 MB working files với 12 worktree là 1.2 GB extra, SSD nuốt được nhưng cứ giả định 10x working size cho batch lớn.
Về latency, spawn 12 agent song song mất khoảng 5-10 giây (CC phải tạo worktree, init context, fetch tool list), không instant. Với batch lớn hơn 30 agent có thể mất 30 giây setup. Sau đó tool loop chạy đều. Reconnect SSH plus claude agents từ phone mất 1-2 giây để daemon trả roster, tốt hơn tmux ls plus attach plus scan từng pane.
Bốn pitfall tôi đã vấp
Vấp đủ rồi nên liệt kê đây cho bạn không phải vấp lại.
Pitfall đầu là parent vẫn idle khi tất cả agent done. Sync session tự kết thúc khi task xong, BG session ở trạng thái idle và daemon giữ chờ task tiếp theo. Nếu quên kill, sau một tuần FleetView của bạn có 30 plus session idle, mỗi cái consume RAM. Cleanup định kỳ: claude agents, focus session idle cũ, nhấn k. Hoặc gõ claude daemon prune (CLI mới có từ 2.1.144) tự dọn session idle quá X ngày.
Pitfall thứ hai là agent push branch nhưng quên mở PR. Nếu bạn brief agent prompt “commit và push”, agent push xong rồi dừng và PR không tự mở. Phải brief rõ “commit, push, gh pr create”. Tôi từng spawn 10 agent, một giờ sau thấy 10 branch trên remote và không có PR nào. Phải làm lại tay 10 lần.
Pitfall thứ ba là shared resource collision khi parallel. Tôi từng spawn 8 agent viết 8 bài blog. Mỗi agent đọc memory bằng seriesOrder để biết mình là bài thứ mấy trong series. Tất cả 8 agent cùng đọc cùng lúc, cùng pick seriesOrder: 9 (số rảnh kế tiếp), kết quả 8 bài cùng order 9. Race condition do shared “next available” resource. Fix: pre-allocate trong prompt mỗi agent (“you are agent 3, your seriesOrder is 11”), không để agent tự pick.
Pitfall thứ tư là token budget cắt giữa batch. Spawn 12 agent song song với plan 90 phút. Tới phút 60, account budget hit 5h limit (đặc biệt với plan Pro). Mọi agent đang chạy bị cắt giữa chừng, một số đã commit, một số chưa, recovery rất đau. Mitigation: kiểm tra /cost hoặc claude usage trước khi spawn batch lớn; nếu budget còn ít, dùng Sonnet thay Opus hoặc giảm batch size.
Cheatsheet thường dùng
| Tác vụ | Lệnh |
|---|---|
| Vào BG từ session đang chạy | /bg trong prompt |
| Start BG ngay từ shell | claude --bg "<prompt>" |
| Spawn BG batch programmatic | Parent gọi Agent tool với run_in_background: true |
| Xem list session | claude agents |
| Attach session bằng ID | claude attach <short-id> |
| Kill session đang focus | k trong FleetView |
| Cleanup session idle cũ | claude daemon prune |
| Xem JSON roster cho automation | claude agents --json |
Setting đáng biết:
| Setting | Default | Khi nào đổi |
|---|---|---|
worktree.bgIsolation | worktree on | "none" khi submodule lớn hoặc cache nặng |
worktree.baseRef | head (theo hook) | Giữ head để agent thấy uncommitted work |
cleanupPeriodDays | 7 | Tăng nếu cần giữ worktree subagent lâu hơn |
CC BG khác Cursor BG, Devin ra sao
Câu hỏi tôi nhận nhiều: CC BG khác hai cái kia thế nào.
Khác biệt cốt lõi là deploy model. CC BG chạy local trên máy bạn, daemon là một Node process. Cursor BG và Devin chạy trên cloud sandbox riêng của họ, repo clone lên cloud, agent edit trong sandbox, PR tự push về repo của bạn. Trade-off: CC BG (local) giữ dữ liệu trên máy, không phải lo upload kích thước repo, có thể chạy với LLM local nếu cấu hình; nhược là tốn RAM/disk máy bạn và không xem được session từ máy khác (mỗi máy FleetView riêng, không sync). Cursor/Devin (cloud) không tốn tài nguyên local và xem được từ phone/web, sandbox isolation thật sự với network egress giới hạn; nhược là repo clone lên cloud của họ, phụ thuộc availability service, không dùng được với private LLM.
Quan điểm cá nhân: với repo cá nhân của tôi, CC BG là default vì tôi tin daemon trên máy mình hơn cloud của bên thứ ba. Bài 3 và 4 sẽ đi sâu Cursor BG và Devin, đủ context để bạn quyết định riêng theo workflow của bạn.
Một câu cuối
Nếu bạn đang dùng Claude Code sync mode và chưa thử /bg, một lần thử là đủ để thấy khác biệt, đặc biệt nếu workflow của bạn có bất kỳ phần nào là “spawn task, đi làm việc khác, quay lại check”. BG không thay thế hoàn toàn sync. Có task tôi vẫn ngồi nhìn từng tool call: debug production, hỏi kiến trúc, review code commit. Nhưng task delegate được plus đi xa plus quay về xem kết quả, BG mode tiết kiệm thời gian thật.
Bài thuộc series Background Agents 2026. Series plan tại bài 1: anatomy của async coding.