Post #3 in the Claude Code Toolkit series. Earlier posts: nf-agents (spawning teams), nf-git-workflow (per-repo authorized git modes). This post is about the skill that sits at the start of the memory pipeline: the one that decides where memory is written in the first place.
Update 2026-05-20. Earlier drafts of this post described configuring
autoMemoryDirectorydirectly in.claude/settings.local.json. That field is silently ignored by the binary at local / project scope. Only user-scope (~/.claude/settings.json) and policy-scope honor it. The mechanism that actually works per project is theCLAUDE_COWORK_MEMORY_PATH_OVERRIDEenvironment variable, set in the same file’senvblock. The skill and this post now use that route. The archaeology behind the discovery (binary v2.1.144, two memory resolvers,stringsplusgrep -aoEon a stripped binary) is in autoMemoryDirectory: chia memory theo project hoặc client.
Pointing two projects at the same memory folder so cross-project facts stop being duplicated sounds like a one-line setting. In practice every new repo costs a JSON merge, an absolute-path conversion, a recursive-scan safeguard, and the first time you try the documented field you discover it does nothing. Want to migrate an old per-project memory folder into a shared one? Manual copy, manual dedupe, manual prayer that no file was overwritten. The friction is small on the first project and not-so-small the moment you have six.
nf-memory is the skill that makes that flow take five minutes instead of an afternoon.
The friction this skill solves
Per-project shared memory is great when it works. Point project-a and project-b at ~/.claude/memory/<scope>/, both sessions read and write the same entries, cross-project facts stop being duplicated. The whole memory pipeline depends on getting that one path right.
What is friction is everything around getting the path to apply:
- The documented setting (
autoMemoryDirectory) does not work at project scope. The binary parses the field, sees the scope islocalSettings, and skips it without a warning. The user-visible behavior is “I set the path, memory still writes to the default location, no error, nothing in the logs.” (Full story in the binary archaeology post.) - The working route is the
env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDEfield. It is nested inside anenvobject, which makes it harder to splice with a text-basedEdit. You wantjqfor this. - The env var is consumed verbatim by the memory resolver. It does not expand
~/. Pass~/.claude/memory/personal/and the binary creates a folder literally named~in your cwd. - The folder has to exist before Claude Code loads the project. The skill creates it for you; the binary will not.
- The settings file goes in
.claude/settings.local.json(gitignored, personal), not.claude/settings.json(committed). Wrong file leaks personal paths into the team’s repo. - The local settings file may already exist with unrelated keys (permission allowlists, hooks, other env vars). You cannot blindly
Writeit; you have to merge into theenvobject specifically. - If you point the path at a directory that contains the current project, Claude Code recursively scans the project as memory on every boot. Boot times explode and nothing in the UI tells you why.
- The env block is read at session startup. Editing it mid-session has no effect. The user must close and relaunch.
- You usually have existing memory to migrate from somewhere: the previous shared folder, the default
<cwd>/.claude/memory/, or both. Some files are project-specific, some are generic. A blanketcp -rdoes the wrong thing on every conflict.
By the time I had six projects routing memory through ~/.claude/memory/personal/ and another four through ~/.claude/memory/<client-org>/, doing this by hand for each new repo cost ten minutes per project plus the occasional mistake (usually forgetting to expand ~/ and discovering a literal ~ folder in the project root a week later). The skill fixes the workflow once.
What the skill is
A SKILL.md that drives Claude through a small state machine:
- Resolve the target folder, either from an explicit argument (
/nf-memory personal) or via an interactive picker. - Validate the input (bare name vs path-like) and detect obvious typos against existing folder names.
- Expand
~/to an absolute path. The env var is not tilde-aware, so the skill resolves before writing. - Refuse the configuration if the target equals or contains the current project (recursive-memory safeguard).
- Read
.claude/settings.local.jsonif it exists, mergeenv.CLAUDE_COWORK_MEMORY_PATH_OVERRIDEinto the existingenvobject (or create the object) without touching other keys, then write back. If a legacyautoMemoryDirectoryfield is found at the top level, remove it as dead config. Create the file fresh only if it does not yet exist. - Detect candidate migration sources (the previous shared folder if any, plus the project-local
.claude/memory/if any) and offer to migrate with per-file conflict handling. - Add
.claude/settings.local.jsonto.gitignoreif missing. - Print a clear restart instruction. The env block is merged into
process.envat session startup, so the current session is unaffected. The user must relaunch Claude Code to pick up the new path.
Every decision point runs through AskUserQuestion: pick a folder, confirm apply, pick sources to migrate, resolve filename or topic conflicts. No free-form prompt I have to remember a syntax for six months later.
The pair with nf-dream
nf-memory and nf-dream are the two halves of a memory pipeline.
nf-memory decides where memory lives. It runs once per project. After it runs, every memory write from the host project lands in the shared folder.
nf-dream keeps that folder healthy. It runs periodically (in my setup, every two or three weeks per project): classify each file as Now / Next / Done / Future / Reference / Stale, rewrite MEMORY.md grouped by bucket, archive aged session-end states, write a one-page HANDOFF-<slug>.md that next-week-me reads in 30 seconds. Snapshot first, never rm.
The pair is intentional. nf-memory is the “set up” step (low frequency, write-heavy, requires care). nf-dream is the “tidy up” step (high frequency, mostly read, snapshot-protected). One configures the folder, the other consolidates it. Run nf-memory once, nf-dream forever.
Skip nf-memory and you lose cross-project sharing. Skip nf-dream and the folder becomes a 200-file swamp by month three.
A first run, end to end
You just cloned a new repo for an existing client. You already have a memory folder at ~/.claude/memory/<client-org>/ from two earlier projects with the same client. You want the new project to share it.
$ cd ~/projects/<client-org>/<new-project>
$ /nf-memory
Reading .claude/settings.local.json...
Current CLAUDE_COWORK_MEMORY_PATH_OVERRIDE: not set
Legacy autoMemoryDirectory at top level: not present
Existing memory folders (under ~/.claude/memory/):
<client-org>/ (37 files, last modified 2 hours ago)
<other-org>/ (12 files, last modified 4 days ago)
personal/ (8 files, last modified 3 weeks ago)
> Pick a memory folder.
1. <client-org> (recommended) 37 files
2. <other-org> 12 files
3. personal 8 files
4. Other (type a custom name or path)
Selection: 1
Preview:
Target path: /home/<user>/.claude/memory/<client-org>/ (absolute, ~ expanded)
Status: existing, 37 .md files
Will update: .claude/settings.local.json
env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE = "<absolute path>"
Will remove: (nothing, no legacy field present)
Restart: required after Apply
Apply this configuration? Yes / No
> Yes
Wrote .claude/settings.local.json via jq merge
Verified .gitignore contains .claude/settings.local.json
Migration:
Source detected: <cwd>/.claude/memory/ (project-local, 3 files)
Migrate 3 files into <client-org>/ (source will not be modified)? Yes / No
> Yes
Per-file migration:
api-conventions.md no conflict, copied
decision-log-2026-04.md no conflict, copied
infra-overview.md filename conflict, consolidated with existing
Report:
Copied as-is: 2 files
Consolidated: 1 file
Sources processed: <cwd>/.claude/memory/ (untouched)
MEMORY.md merged: 3 new entries
Next step:
1. Exit this session.
2. Relaunch Claude Code from this project folder.
3. Verify: echo $CLAUDE_COWORK_MEMORY_PATH_OVERRIDE
(should print /home/<user>/.claude/memory/<client-org>/)
4. Save a test memory and check it lands in the new folder.
After the restart, the new project shares the <client-org> folder. Open one of the older sister projects and it sees the new entries too. The source folder (<cwd>/.claude/memory/) stays intact as a backup; delete it by hand later if you want.
The rest walks through the four design decisions inside the skill that took the longest to get right, with the incident behind each.
Decision 1: jq merge over Write, never clobber existing keys
The single biggest source of damage when modifying a settings file is blindly Write-ing an existing file. Write replaces the whole content. If the file had ten other keys, they are gone.
The setting this skill writes is env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE, a nested key under the top-level env object. That nesting matters: a flat top-level key can be spliced reasonably safely with a text-based Edit (insert one line, preserve commas). Splicing into a nested object means reading the file, finding the env key (if any), creating it if missing, and inserting a child key without breaking the parent’s trailing-comma rules. Doable by hand, fragile to keep correct as files evolve.
The skill always checks first:
test -f .claude/settings.local.json
Three branches follow:
-
File does not exist. Safe to
Writea minimal JSON object:{ "env": { "CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": "<absolute path>" } }Ensure
.claude/exists first withmkdir -p. -
File exists with valid JSON. Use
jqto merge. It understands JSON structure, handles the missing-env-parent case, preserves every other key deterministically:ABS_PATH="<absolute path>" TMP=$(mktemp) jq --arg p "$ABS_PATH" \ 'del(.autoMemoryDirectory) | .env = ((.env // {}) + {"CLAUDE_COWORK_MEMORY_PATH_OVERRIDE": $p})' \ .claude/settings.local.json > "$TMP" && mv "$TMP" .claude/settings.local.jsonThree things happen in one expression: (a) any legacy
autoMemoryDirectoryat the top level is removed (dead config that misleads readers), (b) theenvobject is created if missing or merged if present, (c)CLAUDE_COWORK_MEMORY_PATH_OVERRIDEis set or overwritten without touching sibling env keys. -
File exists but is empty or malformed JSON. Fall back to
Writewith the minimal object, but print a warning in chat first so the user knows the file was clobbered (better than failing silently when the file was a stub anyway).
If jq is somehow not available (rare), the fallback is Read plus Edit against the loaded JSON. Never Write an existing populated file.
The incident: an early version of the skill ran Write unconditionally. The user it ran for had a permissions.allow block with about twenty hand-curated entries. The Write wiped them. Took ten minutes to recover from git reflog and re-derive the list from ~/.claude/projects/.../<session-id>.jsonl transcripts. Fix shipped the same day. Later, when the binary archaeology forced the move from autoMemoryDirectory (flat top-level) to env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE (nested), the jq merge became non-optional. The nested case has too many ways to corrupt a hand-curated file with a regex.
The general lesson applies to any tool touching a user state file: settings, history, credentials, package locks. Prefer a structure-aware tool (jq, yq, language-specific parsers) over text edits. The five extra lines of code are non-negotiable.
Decision 2: AskUserQuestion picker over an assumed default
The early prototype had no picker. It auto-defaulted the target folder to ~/.claude/memory/<project-basename>/, with a flag to override. The reasoning was “fewer prompts, faster flow.”
That was the wrong default. The whole point of autoMemoryDirectory is sharing memory across projects. Auto-defaulting to a per-project folder gives you exactly the project-local layout you would have without the setting at all. The skill became a no-op for its main use case.
The fix was an AskUserQuestion picker that lists existing shared folders first, sorted by recently modified, with a “type a custom name or path” option via the auto-provided “Other”. The user picks in 2 seconds. The picker is the primary affordance, not a fallback.
Side benefit: the list of folders is its own gentle prompt. If you forgot you had a <client-org> folder from three months ago and you start a new project for that same client, the picker reminds you so you do not accidentally create a new isolated one.
The picker also surfaces an optional cwd-mapped suggestion. If ~/.claude/references/nf-memory-mappings.md exists (a small file with cwd-prefix => folder-name mappings), the skill finds the longest matching prefix and tags it (recommended). This is the closest the skill comes to an auto-default, and it is opt-in.
Wider lesson: for any setting that is a personal preference, an AskUserQuestion picker beats a magic default. Personal preferences are the wrong thing to encode in code.
Decision 3: Smart merge on migration, not blind copy
When the new project has memory at <cwd>/.claude/memory/ (the old default), and the new target is ~/.claude/memory/<scope>/, the skill offers to migrate. The naive implementation is cp -r. It is wrong.
The conflicts you hit immediately:
- Both folders have a
MEMORY.md.cpoverwrites the target’s index with the source’s. - Both folders have a file with the same name (
project_session_end_state_<date>.md,api-conventions.md).cpoverwrites whichever the user worked on most recently. - Different filenames describe the same topic.
cpcreates two files about one decision, and the next session has no idea which to trust.
The skill walks per-file:
- No conflict (target lacks the filename, and target’s
MEMORY.mdindex does not describe the topic) → copy mechanically. - Filename conflict →
Readboth, write a consolidated version into target. Prefer newer / more specific entries. - Topic overlap (different filenames, same subject, detected via target’s
MEMORY.mddescriptions) →Readboth, write consolidated. Pick whichever filename is clearer. - Contradiction (sources disagree on a fact) → do not silently pick.
AskUserQuestionwith both versions presented, plus a “Skip this file” option.
MEMORY.md itself is merged last: read each source’s index, read the target’s, dedupe entries by file path, write the union back.
The critical rule: sources are read-only. The skill never modifies, moves, or deletes a source file. If migration breaks something, the original folder is still there. The user deletes it by hand later, after verifying the migration worked. That paranoia is the feature.
The incident: an earlier version moved source files into archive/ after copying. Sounds tidy. The first user who hit a migration mid-laptop-reboot ended up with half the files moved, half still in source, no easy way to retry. Switched to read-only sources the next day. Migration is idempotent now: re-run it and it no-ops on already-copied files.
Decision 4: Refuse to point at the same folder twice (recursive-memory safeguard)
The foot-gun: you set autoMemoryDirectory to ~/.claude/memory/<scope>/. You then cd into ~/.claude/memory/<scope>/ to organize files. You boot Claude Code from inside that folder.
Claude Code reads .claude/settings.local.json (which may exist here if you have ever run the skill from this directory). The setting points at . itself. The session loads every file as memory. Every memory write creates an entry about memory. Boot times explode. Memory traffic becomes recursive.
The skill detects this before applying:
If the resolved target path equals the current cwd
OR the resolved target path is a parent of the cwd
→ refuse with a clear error,
ask via AskUserQuestion for a different path or cancel.
Two realpath resolves and a substring compare. Cheap. The benefit is enormous: the failure mode is invisible (no error, just slow sessions), so catching it at configuration time matters more than catching it at runtime would.
The incident: I once spent twenty minutes debugging why a session loaded slowly in a directory I was using to manually clean memory files. The cwd was inside ~/.claude/memory/personal/. An earlier session had set autoMemoryDirectory: ~/.claude/memory/personal/ via the skill from inside that same folder. Obvious in retrospect; not obvious at all in the moment.
The safeguard is now the last gate before Apply.
General lesson: for any tool that wires a config key A to a filesystem location B, check whether A could resolve to a location that contains the tool’s own working scope. Circular configurations rarely throw; they degrade silently.
The conventions these decisions all share
The four decisions above share two conventions that recur across my personal skills:
Read-before-write on user state. Settings, history, credentials, anything the user typed once and expects to still be there. The cost (one extra Read, one extra Edit vs. Write) is trivial. The cost of getting it wrong is “restore from git reflog.”
AskUserQuestion as the primary interaction mechanism. Flags are great when you remember them. You will not. Six weeks later you will be asking what that one flag actually did. The picker answers in 2 seconds, in the transcript, with the choices visible.
Both conventions also apply to nf-dream and nf-git-workflow. Skill-design defaults you adopt once and never regret.
What you should keep even if you never use my skill
Four patterns generalize cleanly:
- Read before write on any file with user state. Settings, history, credentials, lock files. Never
Writea populated file you have not firstRead. - An interactive picker beats a magic default for personal-preference settings. If the right answer depends on the user’s broader workflow, make it a question, not an assumption.
- Migration tools need smart merge, never blind copy. Walk per-file. Detect filename and topic conflicts. Surface contradictions rather than silently picking. Leave sources untouched until the user confirms.
- Detect circular / self-referential configurations early, with a clear error. Wire-it-to-itself bugs rarely throw at runtime; they degrade silently. Catch them at configuration time.
The skill is one encoding of those patterns. The patterns are the value.
Where this fits in the wider rule-and-skill setup
nf-memory configures the env var that decides where memory is written. The companion rule user-memory-frontmatter (in my host setup) tells the session what to write into each new memory file: a canonical YAML block with metadata.type, metadata.project, metadata.cwd, metadata.tags, metadata.related. The skill configures the destination, the rule structures the content, and nf-dream later relies on metadata.project as the source of truth for per-project filtering.
The combination keeps nf-dream near-instant on warm folders because per-file project filtering becomes a YAML parse instead of an in-session classification.
If you want the full story of why this skill picks an env var over the documented autoMemoryDirectory setting (and how the discovery was made), the binary archaeology lives in autoMemoryDirectory: chia memory theo project hoặc client (Vietnamese). The short version: at localSettings scope, Claude Code’s settings resolver is split into two paths, and the one that consumes autoMemoryDirectory deliberately skips local files for security reasons. The env block at the same scope is consumed by a different path that does not have that filter.
Get the skill
White-labeled, ready to drop into ~/.claude/skills/. Published in the claude-skills-toolkit repo alongside nf-agents, nf-dream, and nf-git-workflow.
git clone https://github.com/llawliet11/claude-skills-toolkit.git
cp -r claude-skills-toolkit/nf-memory ~/.claude/skills/
Folder reference: claude-skills-toolkit/nf-memory.
Verify in a new Claude Code session: type /nf-memory and the skill should appear in the slash-command list. The skill’s own README.md covers the .gitignore hygiene check and the recursive-memory safeguard.
If you adopt it, test with a throwaway project first. The success condition is boring: the existing settings survive, memory writes to the intended folder, and a later session can explain exactly why that folder was chosen.