Memory của Claude Code lưu trên filesystem. File-based, per-machine, không có cloud sync tích hợp sẵn. Nếu bạn chỉ có một máy thì không thành vấn đề. Nhưng hầu hết dev ngày nay làm việc trên ít nhất hai thiết bị: một MacBook, một homelab server, hoặc một máy thứ hai. Memory trên MacBook không có mặt trên server, và ngược lại.

Bài 12 đã đề cập auto-memory và cách CC tự ghi nhớ giữa các session. Bài này đi sâu hơn một bước: làm sao để memory đó tồn tại nhất quán trên nhiều máy, bằng cách biến memory folder thành một git submodule trỏ vào repo riêng.

Không cần tool bên thứ ba. Không cần cloud sync service. Chỉ cần git và một repo private.

Vấn đề: memory không đồng bộ giữa các máy

Claude Code lưu memory tại ~/.claude/ (hoặc path do autoMemoryDirectory chỉ định, xem bài 14). Mỗi máy là một install độc lập.

MacBook:                    homelab:
~/.claude/                  ~/.claude/
  memory/                     memory/
    personal/                   personal/
      MEMORY.md (100 dòng)        MEMORY.md (60 dòng -- lạc hậu)
    project-a/                  project-a/
      MEMORY.md                   (missing)

Sau 3 tháng làm việc: MacBook có 80 memory entry, server có 30, và chúng không còn track cùng một trạng thái nữa. Mỗi lần làm việc trên máy khác, bạn phải nhắc lại các quyết định mà CC trên máy kia đã biết.

Giải pháp đơn giản nhất là theo dõi folder memory/ bằng git, nhưng không phải bằng cách gộp vào repo CC config (sẽ giải thích tại sao ở phần sau). Thay vào đó: biến memory/ thành git submodule trỏ vào một repo riêng biệt.

Kiến trúc: hai repo, một pointer

~/.claude/  (repo: your-account/claudecode)
    settings.json
    rules/
    skills/
    memory/  <-- gitlink (submodule pointer)
        |
        +---> your-account/claude-memory  (repo riêng)
                  personal/
                  project-a/
                  project-b/

~/.claude/ là repo CC config, theo mô hình branch-per-machine (xem bài 22). Mỗi máy có branch riêng (main, homelab, m4…) để tránh xung đột config.

claude-memory là repo memory riêng, dùng single main branch cho tất cả máy. Lý do: memory mostly additive (mỗi entry là một file mới), không thay đổi cùng một file từ nhiều máy đồng thời. Conflict hiếm. Branch-per-machine không cần thiết và chỉ tạo thêm friction.

Kết quả: hai vòng lifecycle độc lập. Config thay đổi trên mỗi máy riêng. Memory đồng bộ qua main chung.

Setup: thêm submodule lần đầu

Trên máy chủ (máy đầu tiên setup):

Bước 1: Tạo repo memory trống trên GitHub/GitLab (private, claude-memory).

Bước 2: Trong ~/.claude/, thêm submodule:

cd ~/.claude
git submodule add https://github.com/your-account/claude-memory memory

Lệnh này tạo folder memory/ là working tree của repo claude-memory, và tạo file .gitmodules trong parent với nội dung:

[submodule "memory"]
    path = memory
    url = https://github.com/your-account/claude-memory

Bước 3: Nếu trước đó memory/ đã tồn tại với nội dung, di chuyển nội dung đó vào:

# Copy các file cũ vào submodule
cp -r /tmp/old-memory/* ~/.claude/memory/

cd ~/.claude/memory
git add .
git commit -m "chore: initial memory import"
git push origin main

Bước 4: Commit parent để track pointer:

cd ~/.claude
git add .gitmodules memory
git commit -m "chore(main): add memory submodule"
git push

Init trên máy mới

Khi clone ~/.claude/ lên máy thứ hai (hoặc server), submodule chưa được populate:

# Clone như bình thường
git clone https://github.com/your-account/claudecode ~/.claude

# Init và fetch submodule
git submodule update --init --recursive

Sau lệnh thứ hai, ~/.claude/memory/ sẽ có đủ nội dung từ claude-memory repo, đúng SHA được parent pointer.

Nếu đã pull parent nhưng quên init submodule, ~/.claude/memory/ sẽ là folder trống. CC sẽ hoạt động nhưng memory trống. Dấu hiệu nhận biết: git submodule status memory trả về dòng bắt đầu với - (uninitialized).

Workflow hàng ngày: save memory

Khi CC ghi một memory mới (hoặc bạn tự edit), nó chỉ thay đổi file trong ~/.claude/memory/. Để memory đó tồn tại và sync được, bạn commit và push trong repo memory, không phải trong parent:

cd ~/.claude/memory
git add .
git commit -m "chore: add memory for project-a architecture decision"
git push origin main

Parent CC config repo không cần đụng đến. Memory repo là nguồn sự thật cho memory content.

Trên máy khác, để lấy memory mới nhất:

cd ~/.claude/memory
git pull origin main

Hoặc nếu muốn pull cả parent lẫn memory cùng lúc:

cd ~/.claude
git pull
git submodule update --remote memory

--remote thay vì --init vì submodule đã được init rồi, bạn muốn fetch HEAD mới nhất từ main thay vì SHA cũ mà parent đang point.

Workflow thỉnh thoảng: bump parent pointer

Parent ~/.claude/ chứa một gitlink (SHA pointer) tới memory. Khi memory repo có commit mới, parent không tự cập nhật pointer. Điều đó là bình thường.

Kiểm tra tình trạng:

cd ~/.claude
git submodule status memory

Có ba trạng thái:

PrefixÝ nghĩa
(space)Pointer khớp với HEAD của submodule. Clean.
+Submodule có commit mới hơn pointer trong parent. Drift.
-Submodule chưa được init. Empty.

Drift (+) là bình thường và không cần fix ngay. Parent không cần track từng memory commit. Khi bạn thấy drift đã đủ lớn và muốn snapshot:

cd ~/.claude
git add memory
git commit -m "chore(main): bump memory pointer"
git push

Quan trọng: chỉ bump sau khi đã push memory repo. Nếu bump pointer trỏ tới một SHA chưa được push, máy khác pull parent về sẽ không fetch được SHA đó, làm hỏng submodule init.

Conflict handling

Vì mỗi entry memory thường là một file riêng, conflict thực sự hiếm. Trường hợp có thể xảy ra: cùng edit MEMORY.md trong cùng folder từ hai máy trước khi push.

Xử lý như bất kỳ git conflict bình thường:

cd ~/.claude/memory
git pull origin main
# Nếu conflict:
git diff
# Sửa bằng tay, giữ cả hai entry thay vì chọn một
git add .
git commit -m "chore: merge memory conflict"
git push

Với memory mostly additive (thêm entry mới, ít khi sửa entry cũ), workflow này đủ dùng mà không cần tooling phức tạp hơn.

Anti-pattern: đừng commit secret vào memory

Memory repo là một private repo, nhưng “private” không có nghĩa là an toàn để chứa secret. Một số lý do:

  • Bạn có thể vô tình đổi visibility thành public.
  • Thành viên được cấp quyền truy cập repo có thể xem toàn bộ history.
  • Nếu GitHub bị breach hoặc token bị leak, repo private cũng bị exposed.

CC đôi khi ghi memory có lợi nhất khi nó chứa context thực tế: API endpoint, service name, cách config một tool. Nếu trong đó có key hoặc password thì đó là rủi ro.

Nguyên tắc đơn giản: memory chứa howwhy, không chứa secret. Ví dụ tốt:

# project-a deployment

Deploy qua Cloudflare Pages. Project name: my-app-prod.
Build command: npm run build. Output: dist/.
Cần unset CLOUDFLARE_API_KEY trước khi wrangler deploy (xem rule wrangler).

Ví dụ xấu:

# project-a credentials

CLOUDFLARE_API_TOKEN=abc123...
DB_PASSWORD=hunter2

Credentials thuộc về .local/ (gitignored), không phải memory.

Tóm tắt và bài tiếp theo

  • Memory của CC là file-based và per-machine. Không có cloud sync tích hợp.
  • Giải pháp: biến ~/.claude/memory/ thành git submodule trỏ vào repo riêng (claude-memory). Hai lifecycle độc lập: config repo branch-per-machine, memory repo single main.
  • Save memory: commit và push trong claude-memory repo, không cần touch parent.
  • Init trên máy mới: git submodule update --init --recursive sau khi clone parent.
  • Bump pointer khi cần snapshot: git add memory && git commit, nhưng chỉ sau khi memory đã được push.
  • Conflict hiếm vì memory mostly additive. Xử lý như git conflict bình thường.
  • Không commit secret vào memory, dù repo là private.

Bài 14 sẽ đi vào autoMemoryDirectory: cách chỉ định cho từng project dùng memory folder riêng, để memory của project A không trộn vào context của project B khi làm việc trên cùng máy.


Bài thuộc series Claude Code từ zero. Series plan tại bài giới thiệu.