Bài 1 đã nhắc đến một chi tiết nhỏ: state của CC là per-machine. Cài CC trên hai máy là có hai bộ config độc lập. Nếu bạn chỉ dùng một máy, điều đó không thành vấn đề. Nhưng nhiều dev có setup nhiều hơn một thiết bị, và “độc lập” nhanh chóng trở thành “lạc nhau”.
Bài này trình bày pattern branch-per-machine: cách dùng git để sync ~/.claude/ giữa nhiều máy mà không bị đau đầu vì merge conflict, và cách một skill nhỏ có thể tự động hóa phần việc tẻ nhạt nhất.
Vấn đề: drift giữa các máy
~/.claude/ là một thư mục bình thường. Bạn có thể git init nó, push lên GitHub private, rồi git pull ở mỗi máy. Về lý thuyết là xong.
Thực tế thì không đơn giản vậy. Vấn đề nằm ở những file có chứa path tuyệt đối hoặc setting gắn với phần cứng cụ thể:
settings.jsoncó thể chứa path"/Users/yourname/..."trỏ đến binary, script, hoặc MCP server chỉ có ở một máy.settings.local.json(trong.claude/nested, gitignored) chứa MCP enable/disable theo từng máy. Máy M3 Max có MCPshared-contextenable, homelab server thì không (vì không có GUI để dùng).- Hook script trỏ đến binary khác nhau (
/opt/homebrew/bin/trên macOS,/usr/local/bin/trên Linux). - Skills có thể dùng path hard-code trong snippet ví dụ.
Nếu bạn dùng main branch chung cho mọi máy và commit settings.json từ máy macOS, lần sau pull ở homelab Linux sẽ nhận về path /Users/yourname/... không tồn tại. Hoặc tệ hơn: bạn edit settings.json ở cả hai máy, lần sau sync sẽ tạo merge conflict ở file đó.
+---------------+ +------------------+
| MacBook M3 | | homelab server |
| | | |
| settings.json | | settings.json |
| (macOS paths) | <-?-> | (Linux paths) |
| | | |
+---------------+ +------------------+
| |
| git merge conflict |
+--------------------------+
Merge conflict ở config file xảy ra thường xuyên, merge thủ công mỗi lần = overhead đủ để nhiều người bỏ cuộc và không sync nữa.
Branch-per-machine: một branch cho mỗi máy
Pattern đơn giản: thay vì dùng một branch chung, mỗi máy có branch riêng của nó.
remote: origin (GitHub private)
├── main (MacBook M3 Max, machine chính)
├── homelab (homelab server)
└── m4 (MacBook M4, khi có)
Mỗi máy chỉ commit lên branch của nó. Không ai bao giờ force-push lên branch của máy khác.
# Trên MacBook M3 Max
git branch --show-current
# main
git add settings.json rules/my-rule.md
git commit -m "chore(main): add new safety rule"
git push
# Trên homelab
git branch --show-current
# homelab
git add settings.json hooks/my-hook.sh
git commit -m "chore(homelab): add linux-specific hook"
git push
Commit message prefix là tên branch vì các branch này không có ticket ID, như đã đề cập ở bài 10 về convention git trong ~/.claude/.
Khi nào cần sync từ máy khác
Bạn viết một rule mới trên MacBook, muốn homelab cũng có rule đó. Hoặc ngược lại: thêm hook trên homelab, muốn MacBook cũng có.
Không cherry-pick nguyên settings.json (vì path sẽ sai), nhưng có thể cherry-pick các file chỉ chứa logic thuần như rules, skills, hooks.
Trường hợp 1: file không có path-sensitive content
# Đang ở homelab, muốn lấy rule mới từ branch main (MacBook)
git fetch origin
git checkout origin/main -- rules/my-new-rule.md
git add rules/my-new-rule.md
git commit -m "chore(homelab): sync rule from main"
Trường hợp 2: muốn xem diff trước
git fetch origin
git diff HEAD origin/main -- rules/
Xem diff, quyết định file nào an toàn để sync.
Trường hợp 3: settings.json có thay đổi logic nhưng cũng có path khác nhau
Đây là trường hợp phức tạp nhất. Giải pháp: merge key-level thủ công. Lấy giá trị từ origin/main cho các key không phụ thuộc path (ví dụ: permissions, env, language), giữ nguyên giá trị local cho các key có path hoặc plugin list (vì plugin enable/disable khác nhau giữa máy).
# Xem settings từ main, copy key cần thiết bằng tay
git show origin/main:settings.json | jq '.permissions'
Không có shortcut nào ở đây. Merge key-level cần đọc và quyết định.
settings.local.json: chỗ để cô lập machine-specific setting
Thay vì nhồi tất cả vào settings.json (tracked), bạn có thể tách machine-specific config ra settings.local.json. File này nằm trong .claude/ nested (gitignored), không bao giờ sync.
Ví dụ:
settings.json(tracked): permissions, hooks, env vars không path-dependent, language.settings.local.json(gitignored): MCP servers enable/disable, path tới binary cục bộ, feature flags chỉ cần ở máy này.
Khi CC khởi động, nó merge settings.json với settings.local.json. Local thắng khi conflict. Cơ chế này cho phép bạn có một settings.json tương đối “sạch” và có thể sync, trong khi machine-specific detail ở local không bao giờ leak vào branch khác.
~/.claude/
├── settings.json <- tracked, dùng chung logic
└── .claude/
└── settings.local.json <- gitignored, per-machine
Skill nf-cc-sync: tự động hóa sync
Sync thủ công bằng git checkout origin/<branch> -- <file> ổn khi bạn biết chính xác mình muốn lấy gì. Nhưng khi có nhiều file cần sync và bạn không chắc cái nào thay đổi, một skill helper sẽ tiện hơn.
Pattern điển hình của skill loại này:
- Detect branch hiện tại (để biết đang ở máy nào).
- Pick branch “nguồn” (máy khác).
- Tính per-file diff giữa local và nguồn.
- Áp dụng safe default:
- File mới hoàn toàn (chỉ có ở nguồn): add an toàn.
- File có ở cả hai: prefer-newer theo timestamp commit.
settings.json: merge key-level thay vì ghi đè toàn file.
- Auto-commit kết quả trên branch local, push.
Ràng buộc quan trọng: skill chỉ pull từ branch máy khác vào local. Không bao giờ push ngược lên branch của máy khác. Branch kia là “lãnh địa” của máy kia.
origin/homelab --> (pull) --> origin/main
^
chỉ chiều này
KHÔNG push ngược
Anti-pattern cần tránh: sau khi sync xong, tự ý push lên origin/homelab với lý do “cập nhật nó cho nhất quán”. Homelab có setting riêng của nó. Bạn chỉ biết setting của máy bạn đang dùng.
Memory submodule: cơ chế riêng
Bài 13 đã trình bày cách memory/ hoạt động như một git submodule. Điểm quan trọng cần nhắc lại trong context bài này: memory không theo branch-per-machine.
~/.claude/ (parent repo)
├── main branch <- MacBook M3 Max
├── homelab branch <- homelab server
└── memory/ <- submodule, luôn dùng branch main
└── (claude-memory repo)
└── main <- shared cross-machine
Memory là single main branch, tất cả các máy đều push/pull vào đó. Lý do: memory chứa fact chung về dự án và preference của user, không phải config machine-specific. Bạn muốn homelab “nhớ” cùng điều mà MacBook nhớ.
Khi pull ở máy mới sau khi đồng bộ parent repo:
git pull
git submodule update --init --recursive
Dòng thứ hai lấy pinned SHA của submodule mà parent vừa nhận về.
Đừng nhầm hai cơ chế: branch-per-machine cho parent config, single-main cho memory submodule.
Workflow hàng ngày
Tóm gọn thành thói quen thực tế:
Trên máy chính (ngày thường):
1. Sửa rule / skill / hook theo nhu cầu
2. Commit lên branch của máy này
3. Push (convention: auto-push sau commit ở repo này)
Khi muốn lấy thứ gì từ máy khác:
1. git fetch origin
2. git diff HEAD origin/<máy-kia> -- rules/ skills/ hooks/
3. Cherry-pick file cụ thể bằng git checkout origin/<máy-kia> -- <file>
4. Review, adjust nếu có path-sensitive content
5. Commit + push
Khi setup máy mới:
1. git clone <repo> ~/.claude
2. git checkout -b <tên-máy-mới>
3. Tạo settings.local.json với machine-specific config
4. git push -u origin <tên-máy-mới>
Máy mới bắt đầu từ HEAD của branch nào bạn clone về (thường là main), sau đó diverge dần khi bạn thêm setting riêng.
Tại sao không dùng symlink hoặc Chezmoi?
Câu hỏi hợp lý. Một số người dùng tool dotfile manager như Chezmoi, hoặc symlink từ repo chung vào ~/.claude/. Cũng được, nhưng có trade-off:
| Approach | Ưu điểm | Nhược điểm |
|---|---|---|
| Branch-per-machine (bài này) | Native git, không tool thêm, máy mới chỉ cần git clone | Sync thủ công hoặc cần skill helper |
| Chezmoi / dotfile manager | Quản lý template biến per-machine tốt hơn | Thêm dependency, convention riêng cần học |
| Symlink từ repo chung | Đơn giản nếu không có path-sensitive content | Không xử lý được khi file thực sự khác nhau giữa máy |
Branch-per-machine phù hợp nhất khi: bạn đã dùng git hàng ngày, không muốn thêm tool, và sự khác nhau giữa các máy chủ yếu nằm ở một số key trong settings.json chứ không phải cấu trúc hoàn toàn khác nhau.
Tóm tắt và bài tiếp theo
- Sync
~/.claude/bằnggit pulltrên branch chung gây merge conflict vì settings có path và config machine-specific. - Pattern branch-per-machine: một branch git cho mỗi máy. Mỗi máy chỉ commit lên branch của nó, không push ngược lên branch máy khác.
- Machine-specific config nên tách sang
settings.local.json(gitignored). Trackedsettings.jsongiữ logic chung. - Khi sync từ máy khác: cherry-pick file cụ thể, merge key-level cho
settings.json, không checkout nguyên branch. - Memory submodule dùng single
mainbranch cross-machine, không theo branch-per-machine như parent repo.
Bài 23 sẽ rời khỏi desktop và nói về một pattern khác: dùng CC từ điện thoại qua Telegram bridge, khi bạn không có máy tính bên cạnh nhưng vẫn muốn kick off một task dài.
Bài thuộc series Claude Code từ zero. Series plan tại bài giới thiệu.