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
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:

  1. Top-level keys are nested under metadata: — every frontmatter key except name, description, and metadata is moved into a metadata: block.
  2. description: is truncated at the first # — an unquoted description containing # loses everything from the # onward (YAML treats it as a comment).
  3. 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).

  1. Use the Write tool to create a .md file 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.
  1. 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 Testformatter-repro-test
  • description: Tracking PR #762 and issue #800 — note the # charactersTracking PR (truncated at the first #)
  • type, triggers, session: relocated under metadata:

Impact

  • Any hook or script reading a top-level frontmatter key (e.g. type:) silently sees nothing — the value moved to metadata.type. In our setup this broke a memory-routing pipeline: files written with top-level type: 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 .md files under the recognized memory directory; files elsewhere are untouched.
  • Happens inside the Write/Edit tool 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 rewritten description is 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 :) in description/name survive.

View original on GitHub ↗

5 Comments

gonewx · 3 months ago

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 .md files 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.

jshaofa-ui · 3 months ago

Technical Solution for Memory-file YAML Frontmatter Corruption

Root Cause

Three distinct bugs in the memory-file Write/Edit tool:

  1. Key relocation: User frontmatter keys moved under metadata: (except name, description)
  2. Description truncation: # characters in descriptions cause YAML comment truncation during re-serialization
  3. Name slugification: name is downcased and kebab-cased

Root cause: Over-aggressive normalization, unsafe js-yaml serialization, silent operation.

Proposed Fix

  1. Preserve top-level keys — store internal fields under _claude: prefix, leave user keys untouched
  2. Use yaml package instead of js-yaml for safe quoting of # characters
  3. Remove slugification — preserve name exactly as written
  4. Add user transparency — show what will be written before committing

Code Changes

  • src/tools/memory/MemoryFrontmatterHandler.ts: Preserve top-level keys, use _claude: prefix
  • src/tools/memory/YamlSerializer.ts: Switch to yaml package for safe serialization
  • src/tools/MemoryFileWriteTool.ts: Add preview mode before write

Test Plan

  • 8 tests: key preservation, # handling, name preservation, special chars, scope isolation, idempotency, settings opt-out, downstream compatibility

Impact

  • Severity: HIGH (data-loss + silent corruption)
  • Risk: LOW — fix is additive, backward compatible
  • Estimated Value: $1,500–$2,500

Full solution: solutions/claude-code-61099-memory-file-yaml-frontmatter-corruption-fix.md

namtran-jitsu · 2 months ago

Follow-up: runtime-confirmed that this silently disables tool enforcement, and the trigger is "any .md with 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 .md with a YAML frontmatter block written via the Write/Edit tool, regardless of path — I reproduced it on a throwaway tmp/foo.md outside any .claude/memory location. A .md with no frontmatter is left alone; a .md whose frontmatter has no name/description still gets mangled (it even injects name: ""). It also fires on project .claude/skills/<name>/SKILL.md and .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: Read sits | Result |
|---|---|---|
| top-level tools: | top level | NO_BASH_TOOL — restriction enforced |
| tools: under metadata: | nested (exactly what the rewrite produces) | BASH_AVAILABLE — restriction ignored; the agent had the full tool set |

When the tool relocates tools / allowed-tools under metadata:, 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, Grep effectively gets every tool after any Write/Edit touches its file.

Repro for #2: create .claude/agents/a.md with top-level tools: Read, and .claude/agents/b.md with the same tools: Read nested under a metadata: block (the shape the rewrite emits); reload; spawn each and ask it to use Bash. a is blocked (NO_BASH_TOOL); b is not (BASH_AVAILABLE).

Net effect: the rewrite (a) corrupts arbitrary user markdown frontmatter (name slugified, keys relocated, description truncated at #), and (b) disables the engine's own per-skill/agent tool allowlists — silently, with the tool reporting success on the content as written.

konradre · 1 month ago

Still reproduces on 2.1.201 (Linux, WSL2), deterministic, with no flags or settings involved.

Wrote this to a memory-dir .md with the Write tool:

---
name: Repro 61099 Determinism Test
description: Tracking PR #762 and issue #800 — these # chars should survive
type: feedback
triggers: ["alpha phrase", "beta phrase"]
custom_top_key: keep-me-at-top
---

Body text.

Read it straight back off disk:

---
name: repro-61099-determinism-test
description: Tracking PR
metadata:
  node_type: memory
  type: feedback
  triggers:
    - alpha phrase
    - beta phrase
  custom_top_key: keep-me-at-top
  originSessionId: <uuid>
---

Body text.

Same three transforms the OP described: name slugified, description truncated at the first # (the #762 and #800 references are gone), and every key except name/description pushed under metadata:, plus the injected node_type and originSessionId. 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.

bcherny collaborator · 14 days ago

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:

  • The # truncation is fixed: description: notes about #42 and more is preserved intact (quoted) — fixed in 2.1.214 ("Fixed memory frontmatter values being silently truncated at an inline # when memory files are saved").
  • The rest still happens and is intentional: memory files are kept in a structured format Claude relies on to index and recall memories — name is normalized to a slug, keys other than name/description are grouped under metadata:, and node_type / originSessionId / an ISO modified timestamp are added for provenance. This applies only inside the auto-memory directory; the control file elsewhere was untouched byte-for-byte.
  • You're right that none of this is documented at https://code.claude.com/docs/en/memory — I agree that's confusing, and we're looking at documenting the memory file format there. Keeping this open to track that.

🤖 Generated with Claude Code