Memory-file Write/Edit silently rewrites YAML frontmatter (nests top-level keys, truncates `description` at #, slugifies `name`)
Status Open
Reported on v2.1.146
Maintainer reply ✓ Yes — bcherny
Workaround ✓ Mentioned in thread ↓
Activity 5 comments · opened May 21, 2026
💡 Likely answer: A maintainer (bcherny, collaborator)
responded on this thread — see the highlighted reply below.
Summary
When a Markdown file under the memory directory is written via the Write or Edit tool, Claude Code silently rewrites its YAML frontmatter before the file reaches disk. Three transformations occur, none requested or opt-out-able:
- Top-level keys are nested under
metadata:— every frontmatter key exceptname,description, andmetadatais moved into ametadata:block. description:is truncated at the first#— an unquoted description containing#loses everything from the#onward (YAML treats it as a comment).name:is slugified — downcased and kebab-cased.
node_type: memory and originSessionId: <uuid> are also injected.
This breaks any tooling — user hooks, sync scripts, downstream parsers — that reads top-level frontmatter fields, because those fields are no longer where they were written.
Reproduction
Claude Code 2.1.146, macOS (arm64).
- Use the
Writetool to create a.mdfile under the memory directory (~/.claude/projects/<project>/memory/) with:
---
name: Formatter Repro Test
description: Tracking PR #762 and issue #800 — note the # characters
type: feedback
triggers: ["alpha phrase", "beta phrase"]
session: false
---
Body text.
- Read the file back from disk.
Expected
Frontmatter preserved as written (whitespace normalization at most).
Actual
---
name: formatter-repro-test
description: Tracking PR
metadata:
node_type: memory
type: feedback
triggers:
- alpha phrase
- beta phrase
session: false
originSessionId: <session-uuid>
---
Body text.
name:Formatter Repro Test→formatter-repro-testdescription:Tracking PR #762 and issue #800 — note the # characters→Tracking PR(truncated at the first#)type,triggers,session: relocated undermetadata:
Impact
- Any hook or script reading a top-level frontmatter key (e.g.
type:) silently sees nothing — the value moved tometadata.type. In our setup this broke a memory-routing pipeline: files written with top-leveltype:failed classification because the router reads top-level only, and silently piled up in an_unrouted/directory. - The
#-truncation is data loss — issue/PR references (#762), or any#in a description, are destroyed. Surprising, since the rest of the description is preserved. - The rewrite is silent: no diff, no notice; the tool reports success with the content the user supplied, while a different thing lands on disk.
Notes
- Only fires for
.mdfiles under the recognized memory directory; files elsewhere are untouched. - Happens inside the
Write/Edittool body, before the file is written — it is not a PostToolUse hook and cannot be disabled via~/.claude/settings.json. - The
#-truncation is a YAML-serialization side effect: the rewrittendescriptionis emitted unquoted, so#starts a comment. Quoting string values on re-serialization would fix that effect.
Suggested fix
A way to preserve frontmatter as authored — either:
- preserve top-level keys (don't relocate them under
metadata:), or a setting to opt out of the memory-file frontmatter normalization; and - always quote string values when re-serializing, so
#(and:) indescription/namesurvive.
5 Comments
Ran into this too — the silent frontmatter rewrite broke a hook pipeline that reads
type:at the top level to route memory files into different indexes. Took a while to notice because the Write tool reports success with the original content, not what actually lands on disk.One workaround if you need predictable frontmatter today: put your memory files outside the recognized memory directory and manage them yourself via hooks. The rewrite only fires for
.mdfiles under~/.claude/projects/<project>/memory/.For anyone building tooling on top of Claude Code session data — the raw JSONL files under
~/.claude/projects/<project-hash>/are untouched by this normalization. I've been indexing those directly with Mantra to get reliable full-text search across sessions without fighting the frontmatter layer.Technical Solution for Memory-file YAML Frontmatter Corruption
Root Cause
Three distinct bugs in the memory-file Write/Edit tool:
metadata:(exceptname,description)#characters in descriptions cause YAML comment truncation during re-serializationnameis downcased and kebab-casedRoot cause: Over-aggressive normalization, unsafe
js-yamlserialization, silent operation.Proposed Fix
_claude:prefix, leave user keys untouchedyamlpackage instead ofjs-yamlfor safe quoting of#charactersnameexactly as writtenCode Changes
src/tools/memory/MemoryFrontmatterHandler.ts: Preserve top-level keys, use_claude:prefixsrc/tools/memory/YamlSerializer.ts: Switch toyamlpackage for safe serializationsrc/tools/MemoryFileWriteTool.ts: Add preview mode before writeTest Plan
#handling, name preservation, special chars, scope isolation, idempotency, settings opt-out, downstream compatibilityImpact
Full solution:
solutions/claude-code-61099-memory-file-yaml-frontmatter-corruption-fix.mdFollow-up: runtime-confirmed that this silently disables tool enforcement, and the trigger is "any
.mdwith frontmatter" (not just the memory dir)Ran a few controlled tests on 2.1.169 (current latest) on a stock install — reproduces with no special flags or settings.
1. Scope is broader than the memory directory. The rewrite fires on any
.mdwith a YAML frontmatter block written via the Write/Edit tool, regardless of path — I reproduced it on a throwawaytmp/foo.mdoutside any.claude/memory location. A.mdwith no frontmatter is left alone; a.mdwhose frontmatter has noname/descriptionstill gets mangled (it even injectsname: ""). It also fires on project.claude/skills/<name>/SKILL.mdand.claude/agents/<name>.md. So this isn't scoped to memory files — it's a blanket frontmatter rewrite. (OP repro'd on 2.1.146; still present on 2.1.169 → unfixed across these versions.)2. It silently breaks Claude Code's own tool enforcement — verified at runtime, not just inferred. Two throwaway subagents, identical except for the frontmatter shape, each declaring
tools: Read(i.e. no Bash). Each was asked to report whether the Bash tool was actually available to it:| Agent | where
tools: Readsits | Result ||---|---|---|
| top-level
tools:| top level |NO_BASH_TOOL— restriction enforced ||
tools:undermetadata:| nested (exactly what the rewrite produces) |BASH_AVAILABLE— restriction ignored; the agent had the full tool set |When the tool relocates
tools/allowed-toolsundermetadata:, the loader (which reads the top-level key only) no longer finds it, so the agent/skill runs unrestricted. This is a permission/safety regression, not a cosmetic change: an agent authored to be confined to e.g.Read, Grepeffectively gets every tool after any Write/Edit touches its file.Repro for #2: create
.claude/agents/a.mdwith top-leveltools: Read, and.claude/agents/b.mdwith the sametools: Readnested under ametadata:block (the shape the rewrite emits); reload; spawn each and ask it to use Bash.ais blocked (NO_BASH_TOOL);bis not (BASH_AVAILABLE).Net effect: the rewrite (a) corrupts arbitrary user markdown frontmatter (name slugified, keys relocated,
descriptiontruncated at#), and (b) disables the engine's own per-skill/agent tool allowlists — silently, with the tool reporting success on the content as written.Still reproduces on 2.1.201 (Linux, WSL2), deterministic, with no flags or settings involved.
Wrote this to a memory-dir
.mdwith the Write tool:Read it straight back off disk:
Same three transforms the OP described:
nameslugified,descriptiontruncated at the first#(the#762and#800references are gone), and every key exceptname/descriptionpushed undermetadata:, plus the injectednode_typeandoriginSessionId. So it holds across 2.1.146, 2.1.169, and now 2.1.201.One thing that might narrow the root cause: the trigger is the Write/Edit tool, not the directory. I dropped a byte-identical file into the same memory folder with a plain shell redirect (
cat > file.md) and it stayed exactly as written, even after sitting untouched for several minutes. Only the copy written through the tool got rewritten, which points to the tool's own write path rather than a filesystem watcher or an indexer.Thanks for the detailed report — I retested this on 2.1.233 (Linux, fresh config dir) by asking Claude to Write your exact frontmatter into the auto-memory directory, with an identical Write to a non-memory path as a control.
What I observed on 2.1.233:
#truncation is fixed:description: notes about #42 and moreis preserved intact (quoted) — fixed in 2.1.214 ("Fixed memory frontmatter values being silently truncated at an inline#when memory files are saved").nameis normalized to a slug, keys other thanname/descriptionare grouped undermetadata:, andnode_type/originSessionId/ an ISOmodifiedtimestamp are added for provenance. This applies only inside the auto-memory directory; the control file elsewhere was untouched byte-for-byte.🤖 Generated with Claude Code