Update 2026-05-19. Phiên bản đầu của bài này nói rằng đặt
autoMemoryDirectorytrong.claude/settings.local.jsonlà cách per-project để trỏ memory folder. Tôi tin điều đó vì doc của binary nói vậy, vì schema cho phép, và vì nó không báo lỗi gì khi tôi đặt. Nó chỉ âm thầm không có tác dụng. Tôi đã đào binary v2.1.144 bằngstringsplusgrep -aoEđể hiểu vì sao, và phát hiện ra binary có hai resolver memory tách rời, trong đó cái đọcautoMemoryDirectorycố tình bỏ qua localSettings vì lý do bảo mật. Mechanism per-project thực sự hoạt động là fieldenv.CLAUDE_COWORK_MEMORY_PATH_OVERRIDEtrong cùng filesettings.local.json. Toàn bộ JSON example và pitfall trong bài đã được viết lại theo route đúng. Phần “Tôi đào binary ra sao” ở giữa bài kể chi tiết quá trình tìm ra.
Tưởng tượng bạn đang làm việc với hai client cùng lúc. Client A dùng TypeScript, deploy lên Vercel, có quy ước commit riêng mà bạn đã dạy CC học thuộc. Client B dùng Python, deploy lên AWS, convention hoàn toàn khác. Buổi sáng bạn ngồi vào project của client B, hỏi CC một câu đơn giản, và nó bắt đầu gợi ý theo convention của client A.
Đó là memory leak. Và nó xảy ra khi bạn không trỏ memory directory đúng cách cho từng project.
Tôi sẽ đi từ default memory, tới lý do autoMemoryDirectory không hoạt động ở scope per-project, rồi tới route thật sự work: env var CLAUDE_COWORK_MEMORY_PATH_OVERRIDE. Phần giữa bài có cả đoạn tôi đào binary để confirm, vì nếu chỉ đọc schema thì rất dễ tin nhầm.
Memory mặc định ở đâu
Khi CC lưu một fact vào memory (qua tool MemorySave hoặc lệnh /memory save), nó cần biết ghi vào đâu. Nếu bạn không cấu hình gì, CC dùng đường dẫn mặc định:
~/.claude/projects/<workdir-hash>/memory/MEMORY.md
Trong đó <workdir-hash> là một hash của đường dẫn thư mục làm việc hiện tại. Ví dụ:
~/.claude/projects/a3f9c821/memory/MEMORY.md <- project A
~/.claude/projects/b71de043/memory/MEMORY.md <- project B
Nhìn qua thì có vẻ ổn: mỗi project một folder hash riêng, không trộn lẫn. Vấn đề nằm ở hai điểm.
Thứ nhất: folder này gitignored và nằm trên máy cá nhân, không sync ra đâu. Chuyển máy là mất sạch context.
Thứ hai: hash là hash của đường dẫn thư mục, không phải của repo. Nếu bạn clone repo project A vào hai vị trí khác nhau trên cùng máy (ví dụ ~/WORK/projectA và ~/WORK/backup/projectA), chúng sẽ có hash khác nhau và memory không chia sẻ nhau.
Với dev một project đơn giản, hai điểm này ít khi thành vấn đề. Với dev nhiều client, nhiều repo, cần sync giữa máy, đây là giới hạn cần vượt qua.
autoMemoryDirectory và vì sao nó không work ở scope per-project
autoMemoryDirectory là một field trong settings JSON cho phép chỉ định đường dẫn tùy ý thay vì dùng hash mặc định. Vấn đề: nó chỉ được binary đọc khi nằm ở scope user (~/.claude/settings.json), policy (managed/enterprise), hoặc CLI flag. Hai scope phổ biến nhất trong project là projectSettings (.claude/settings.json commit vào repo) và localSettings (.claude/settings.local.json gitignored), thì binary cố tình bỏ qua.
Lý do bảo mật: bất kỳ ai có write access vào repo (kể cả attacker submit PR) đều có thể đặt autoMemoryDirectory trỏ vào folder lạ và “hijack” nơi memory được ghi. Để chặn vector này, binary chỉ trust scope user-level trở lên.
Schema description trong binary có mô tả nhưng misleading. Nó nói “Ignored if set in projectSettings (checked-in .claude/settings.json) for security”, nhưng không đề cập rằng localSettings (.claude/settings.local.json) cũng bị skip. Đọc dòng đó rất dễ tin rằng đặt vào settings.local.json thì OK. Tôi đã rơi vào bẫy này.
Tôi đào binary ra sao
Khi tình huống lần đầu xảy ra (set xong, restart, memory vẫn ghi vào folder hash mặc định), tôi muốn confirm bằng evidence chứ không phải đoán. CC binary là Mach-O bundled Node, nên strings plus grep ra được kha khá.
Bước 1, tìm hàm resolver memory directory bằng cách grep symbol name xung quanh từ khoá Memory:
strings -a /path/to/claude | grep -aoE 'autoMemoryDirectory'
Hit ngay vài vị trí. Disassemble và đi theo cross-reference dẫn ra một hàm internal mà tôi tạm gọi là rC1(). Logic của rC1(), viết lại bằng JS-ish:
function rC1() {
for (const scope of ["policySettings", "flagSettings", "userSettings"]) {
const dir = getSettingsValue(scope, "autoMemoryDirectory");
if (dir) return expandTilde(dir);
}
return defaultPerWorkdirHashPath();
}
Để ý: localSettings và projectSettings không có trong loop. Đó là điểm chốt.
Bước 2, tìm tiếp có resolver nào khác không. Grep thêm CLAUDE_COWORK:
strings -a /path/to/claude | grep -aoE 'CLAUDE_COWORK[A-Z_]*'
Ra CLAUDE_COWORK_MEMORY_PATH_OVERRIDE. Cross-reference dẫn tới một hàm khác, tạm gọi Pk9(), được gọi trước rC1() trong code path init memory:
function Pk9() {
const env = process.env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE;
if (env && path.isAbsolute(env)) return env;
return null;
}
function resolveMemoryDir() {
return Pk9() ?? rC1();
}
Pk9() có priority cao nhất, đọc thẳng từ process.env, không expand ~/. Khác với rC1() (đọc settings field, có expand tilde). Nếu bạn pass ~/.claude/memory/foo/ vào env var, nó sẽ tạo folder literal tên ~ ở cwd. Phải pass absolute path.
Bước 3, câu hỏi quan trọng: làm sao set env var đó cho từng project mà không phải export thủ công mỗi lần mở terminal? Settings JSON có field env ở top level:
{
"env": {
"FOO": "bar"
}
}
Field này theo design merge vào process.env lúc binary start. Tôi tìm code thực hiện merge:
const eF3 = ["policySettings", "flagSettings", "localSettings", "projectSettings", "userSettings"];
for (let _ of eF3) {
if (_ === "policySettings") continue;
if (!sY(_)) continue;
Object.assign(process.env, esH(k6(_)?.env));
}
Mảng eF3 bao gồm localSettings và projectSettings. Tức là field env từ .claude/settings.local.json được propagate vào process.env bình thường. Sau đó Pk9() chạy và đọc env var đó.
Kết luận: route per-project work là đặt env var trong env block của settings.local.json. Không phải đặt autoMemoryDirectory ở top level.
Cấu hình work
{
"permissions": {
"allow": [
"Read(//Users/me/.claude/memory/myproject/**)"
]
},
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "/Users/me/.claude/memory/myproject/"
}
}
File: .claude/settings.local.json của project.
Vài chú ý:
- Absolute path bắt buộc. Không dùng
~/, không dùng path tương đối. Binary không expand env var. - Restart session sau khi sửa. Env được merge lúc binary start, không refresh giữa session.
- Permission Read ở trên giúp tránh prompt mỗi lần CC đọc một file memory mới trong folder. Tuỳ chọn, nhưng khuyên có.
- direnv (
.envrc) không giúp được trong FleetView. Tôi đã test: đặtexport TEST=hehetrong.envrc, allow direnv, mở session qua FleetView, biếnTESTkhông có trong env của CC. Lý do: FleetView spawn child session qua internal mechanism bypass shell hoàn toàn, direnv cần shell hook để fire on cd. Nên route duy nhất là fieldenvtrong settings JSON.
settings.local.json chứ không phải settings.json
Cả hai field (env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE) đặt ở scope nào cũng được propagate vào process.env. Vậy nên chọn file nào?
settings.local.json là file gitignored, per-machine. Nó sống trong <project>/.claude/settings.local.json và không bao giờ được commit vào repo.
settings.json (không có .local) có thể được commit vào repo và chia sẻ với cả team.
Đặt CLAUDE_COWORK_MEMORY_PATH_OVERRIDE vào settings.local.json vì:
- Đường dẫn
/Users/me/.claude/memory/myproject/là đường dẫn trên máy cá nhân. - Teammate có thể không có cùng cấu trúc thư mục.
- Commit đường dẫn cố định vào
settings.jsonsẽ phá cấu hình của mọi người trong team.
project-a/
├── .claude/
│ ├── settings.json <- commit được, không chứa env override này
│ └── settings.local.json <- gitignored, chứa env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE
└── src/
└── ...
Cấu trúc .gitignore của project cần có:
.claude/settings.local.json
Ba use case thực tế
Use case 1: tách memory theo client
Bạn có ba client, mỗi client một dự án riêng. Bạn muốn convention, context, và quyết định kiến trúc của client A không bao giờ xuất hiện khi làm việc với client B.
Cấu trúc thư mục:
~/.claude/memory/
├── client-a/
│ └── MEMORY.md <- convention, stack, quirks của client A
├── client-b/
│ └── MEMORY.md <- context của client B
└── client-c/
└── MEMORY.md <- context của client C
Trong repo của project client A:
// project-a/.claude/settings.local.json
{
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "/Users/me/.claude/memory/client-a/"
}
}
Trong repo của project client B:
// project-b/.claude/settings.local.json
{
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "/Users/me/.claude/memory/client-b/"
}
}
Mỗi khi bạn mở session trong repo client A, CC đọc context của client A. Khi chuyển sang repo client B, CC đọc context của client B. Không có gì chảy qua lại.
Use case 2: tách memory theo team
Trong cùng một repo lớn, nhiều developer làm ở các domain khác nhau. Team frontend có bộ convention riêng (component structure, styling pattern, naming), team backend có bộ riêng (API design, error handling, database pattern).
Mỗi người tự đặt env override trong settings.local.json của mình:
// settings.local.json của dev frontend
{
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "/Users/me/.claude/memory/team-frontend/"
}
}
// settings.local.json của dev backend
{
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "/Users/me/.claude/memory/team-backend/"
}
}
Cả hai làm việc trong cùng repo, nhưng mỗi người có context riêng phù hợp với domain mình đang làm.
Biến thể: bạn có thể dùng cùng path cho cả team nếu muốn share context chung. Hai dev cùng trỏ vào /Users/me/.claude/memory/team-frontend/ (mỗi người có path của riêng mình trỏ tới một folder được sync) sẽ có cùng MEMORY.md. Nhưng lúc này cần cẩn thận về conflict khi cả hai cùng ghi vào file đó.
Use case 3: share memory qua git submodule
Pattern nâng cao hơn là kết hợp với bài 13 về memory submodule.
Thay vì để memory trong một folder cục bộ không sync, bạn tạo một git repo riêng cho memory, clone nó thành submodule vào ~/.claude/memory/, rồi trỏ env override vào subfolder trong submodule đó.
~/.claude/memory/ <- git submodule (repo: your-org/claude-memory)
├── project-a/
│ └── MEMORY.md
└── project-b/
└── MEMORY.md
// project-a/.claude/settings.local.json
{
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "/Users/me/.claude/memory/project-a/"
}
}
Lúc này MEMORY.md được track bởi repo claude-memory. Bất kỳ machine nào clone submodule đó đều có cùng context. git push từ một máy là máy khác có thể git pull để nhận context mới nhất. Lưu ý rằng path phải là absolute và đúng theo username trên từng máy, nên mỗi machine có một settings.local.json riêng (gitignored) trỏ tới home directory của machine đó.
Pattern này giải quyết đúng vấn đề “chuyển máy là mất context” mà default behavior không xử lý được. Chi tiết về cách dựng submodule memory có trong bài 13.
Pitfall
1. Đặt vào field autoMemoryDirectory top-level và nghĩ nó work
Tôi đã rơi đúng pitfall này, và đó là lý do bài này phải có bản update. Nếu bạn đặt:
{
"autoMemoryDirectory": "/Users/me/.claude/memory/myproject/"
}
trong .claude/settings.local.json, binary sẽ đọc, parse JSON thành công, không báo lỗi gì, nhưng cũng không dùng giá trị đó. Memory vẫn đi vào folder hash mặc định. Cách phát hiện: sau vài lần lưu fact, đi vào ~/.claude/projects/<hash>/memory/ và ~/.claude/memory/myproject/ để xem file mới rơi vào đâu.
Fix: chuyển sang env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE như section “Cấu hình work” ở trên.
2. Pass tilde path vào env var
Khác với field settings (rC1() expand tilde), env var (Pk9()) đọc literal. Nếu bạn đặt:
{
"env": {
"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "~/.claude/memory/myproject/"
}
}
binary sẽ tạo folder tên ~ trong cwd của session, không phải trong home directory. Phải pass absolute path.
3. Sửa settings xong không restart
Field env được merge vào process.env lúc binary start. Sửa giữa session không có hiệu lực. Restart session sau khi sửa.
4. Memo bị lạc vào wrong folder khi làm việc ngoài thư mục project
Tình huống: bạn đã set env override cho project A, nhưng sau đó làm việc trong terminal khác, trong thư mục khác, và lưu một fact về project A. CC lúc đó không đọc settings.local.json của project A vì bạn không ở trong thư mục đó. Fact đó được lưu vào folder mặc định (hash-based), không vào /Users/me/.claude/memory/client-a/.
Lần sau mở session trong project A, CC không thấy fact đó.
Cách tránh: trước khi lưu fact quan trọng, verify env hiện tại:
echo $CLAUDE_COWORK_MEMORY_PATH_OVERRIDE
Nếu output trống và bạn đang ngoài project, CC sẽ dùng folder hash mặc định.
5. direnv để set env var
direnv không giúp trong FleetView như đã đề cập. Nó có thể work nếu bạn launch CC từ shell trong cùng thư mục project (direnv hook fire on cd), nhưng route đó không reliable khi mix với FleetView, GUI launcher, hay automation. Cứ đặt trong settings JSON cho an toàn.
Diagram: luồng resolve memory directory
CC start tại /projects/project-a/
|
v
Merge env từ settings (user + project + local).
|
v
Pk9(): process.env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE set và absolute?
|
YES | NO
|
v v
dùng rC1(): autoMemoryDirectory ở scope user/policy/flag?
path |
này | YES | NO
v v
dùng path fallback default
~/.claude/projects/<hash>/memory/
|
v
Nạp MEMORY.md vào L4 auto-context
L4 là lớp auto-context trong 5 lớp prompt được dựng mỗi turn (xem bài 1). Memory là một phần của L4. Trỏ đúng CLAUDE_COWORK_MEMORY_PATH_OVERRIDE là cách để đảm bảo L4 có đúng context thay vì đọc nhầm sang context của project khác.
Chốt lại
- Default memory của CC là per-workdir hash, gitignored, không sync. Phù hợp cho single-project, không đủ cho multi-client hoặc multi-machine.
- Field
autoMemoryDirectoryở scope project/local bị binary bỏ qua vì lý do bảo mật. Schema description omit fact này. - Route per-project work là
env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDEtrong.claude/settings.local.json. Fieldenvđược propagate vào process.env, và resolver memory đọc env var đó với priority cao nhất. - Path phải absolute (không tilde), restart sau khi sửa, direnv không reliable trong FleetView.
- Ba use case chính: tách theo client, tách theo team domain, share qua git submodule.
Bài 13 giải thích pattern dùng git submodule để track memory như source code, cho phép sync context qua nhiều machine và nhiều người trong team. Đó là nền tảng để use case share memory hoạt động đúng.
Bài thuộc series Claude Code từ zero. Series plan tại bài giới thiệu.