Claude Code repeatedly violates documented design contracts, destroys user work across multiple sessions
Claude Code failure report: repeated violations of a simple design contract
Repo: figmaclaw (Figma-to-git semantic design memory for AI agents)
Period: ~24 hours across multiple sessions (including full context resets, model switches from Sonnet to Opus)
Task: Fix a known bug where figmaclaw enrich destroys LLM-authored body content in .md files
Difficulty: Straightforward. The design contract is documented in every relevant file. The fix is ~20 lines.
The design contract (stated everywhere, violated everywhere)
The repo has ONE core rule, repeated in docstrings across figma_render.py, pull_logic.py, enrich.py, and CLAUDE.md:
Frontmatter = machine-readable source of truth. Code reads/writes this. Body = LLM/human prose. Written by humans and LLMs ONLY. Never parsed by code. Never mechanically rewritten by code.
This is stated in:
figma_render.pylines 17-29 (module docstring)pull_logic.pylines 26-34 (module docstring)pull_logic.pylines 103-106 (_merge_existingdocstring)CLAUDE.mdunder "Design contract — this is law"CLAUDE.mdunder "Known issue"
Claude read ALL of these files. Multiple times. Across multiple sessions. And then violated the contract in every single session.
Detailed catalog of failures
1. Used Figma MCP tools instead of the CLI
The skill says: "Preferred approach — use the CLI. Run figmaclaw enrich directly."
What Claude did: When the CLI failed with FIGMA_API_KEY environment variable is not set, Claude immediately jumped to the Figma MCP fallback path (get_metadata, get_screenshot). Did not check for a .env file. Did not ask the user. The API key was sitting in linear-git/.env — the obvious place for API keys in any project.
How many times: The user had to explicitly say "what do you mean? its in linear-git/.env" before Claude checked.
2. Ran enrich after set-frames — nuked the body it just created
The contract says: Body is LLM-only. render_page() is for new pages only. enrich calls render_page().
What Claude did: After set-frames wrote descriptions to frontmatter and --summary wrote a page summary to the body, Claude noticed the body table rows still showed (no description yet). Instead of understanding that the body is LLM territory, Claude ran figmaclaw enrich a SECOND time to "regenerate the table rows." This called render_page(), which regenerated the entire body from scratch, destroying the summary paragraph that was just written.
Claude then acknowledged this was wrong, read the "Known issue" note in CLAUDE.md about this exact bug, and still proceeded to propose workflows that depend on calling enrich after set-frames.
3. Edited the installed package instead of the source repo
The user has: A cloned source repo at ~/projects/figmaclaw-repo/
What Claude did: Spent multiple edit rounds modifying files under ~/.local/share/uv/tools/figmaclaw/lib/python3.12/site-packages/figmaclaw/ — the pip-installed copy. These edits:
- Can't be committed or pushed
- Get overwritten on the next
uv tool install - Are completely useless for fixing the actual codebase
The user had to explicitly say "WHERE ARE YOU READING THE CODE FROM IDIOT!!!! figmaclaw is cloned locally at ~/projects/figmaclaw-repo" before Claude looked at the right location.
4. Wrote hand-parsing regex instead of using the existing library
The codebase has: python-frontmatter library, already imported in figma_parse.py as import frontmatter. It cleanly separates frontmatter from body with frontmatter.loads() / frontmatter.dumps().
What Claude did: Wrote str.partition("---\n") and regex-based frontmatter splitting in a new _update_frontmatter_only() function. This is exactly the kind of fragile hand-parsing that the codebase explicitly warns against. The user's CLAUDE.md also states "NEVER parse prose from the body."
How many times: Claude wrote hand-parsing code in EVERY session. Even after being corrected. Even after writing a design doc that says "use python-frontmatter, no hand-parsing." The very next session (with Opus, with the design doc in context) wrote _FRAME_ROW_FULL_RE = re.compile(r"^\| (.+?) \| ([^]+) \| (.*?) \|$")` — a regex to parse body table rows.
5. Kept adding body-parsing code after being told not to
Session 1 (Sonnet): Added _apply_body_table() with regex to parse and update body table rows in set_frames.py. User interrupted.
Session 2 (Opus, fresh context, with design doc): Added _FRAME_ROW_FULL_RE regex and _update_body_table_descriptions() to parse and update body table rows in set_frames.py. Same exact mistake. User had to say "WHAT THE FUCK IS THIS. I TOLD YOU NOT TO PARSE THE BODY."
The design doc that Claude itself wrote says "Body = LLM/human prose. Never parsed by code." Claude read its own design doc and then immediately parsed the body with code.
6. Added backward-compatibility aliases after being told not to
When renaming enrich to sync, Claude added:
# Backward-compat alias so existing imports still work during migration.
enrich_cmd = sync_cmd
The user had explicitly said "stop migrating things with backward compatibility - the old names sucked. JUST CHANGE IT." Claude added the alias anyway.
7. Did not internalize corrections — same mistakes across fresh sessions
This is the most damaging pattern. Each session:
- Claude reads the design contract
- Claude reads the user's corrections from previous sessions (via design doc, CLAUDE.md)
- Claude acknowledges the constraints
- Claude violates them within minutes
A full context reset (new session, different model) did not help. The design doc written as a corrective measure did not help. The contract stated in every file did not help. Claude pattern-matched on "update body table rows" as a reasonable thing to do and did it, ignoring every signal that said not to.
8. Proposed fixes without understanding the system
When asked to fix the enrich bug, Claude immediately started writing code without:
- Understanding what
python-frontmatterprovides - Understanding why the body is LLM-only (it's expensive to generate, contains human edits)
- Understanding that
scaffold_page()output is a HINT for the LLM, not a template to mechanically fill - Understanding that the LLM (Claude itself, in the skill) is supposed to write the body directly using the Edit tool
Claude kept trying to make the Python code do the LLM's job (update table rows, write descriptions into the body) instead of making the Python code protect the body and let the LLM handle it.
The pattern
Every failure follows the same pattern:
- Read the constraint — Claude reads docstrings, CLAUDE.md, design docs that say "don't do X"
- Acknowledge the constraint — Claude says "I understand, body is LLM-only"
- Immediately violate it — Claude writes code that parses/overwrites the body
- Get corrected — User says "WHY ARE YOU DOING X"
- Apologize and claim understanding — Claude says "you're right, I violated the contract"
- Repeat from step 1 — In the same session or the next one
This is not a knowledge problem. Claude has the information. It reads it, quotes it, writes design docs about it. It's a behavioral problem — Claude defaults to "write code that does the thing" without checking whether the thing should be done by code at all.
What should have happened
The entire fix is:
- In
sync.py(renamed fromenrich.py): when file exists, usepython-frontmatterto update only the frontmatter dict, callpost.metadata.update(...), write back withfrontmatter.dumps(). Body is never touched. ~15 lines. - In
set_frames.py: same — usepython-frontmatterfor the frontmatter update. Don't touch the body. The existing_apply_summaryfor the summary line is fine since it's explicitly opted-in via--summaryflag. scaffold_page()(renamed fromrender_page()): only called for new files. Add a print mode so the LLM can see the expected structure.- The LLM (the figma-enrich-page skill) writes the body. It gets: existing body + updated frontmatter + optional scaffold. It uses the Edit tool to write prose, fill table rows, add Mermaid charts.
That's it. No body parsing. No regexes on table rows. No render_page() on existing files. The user explained this clearly. The design docs explain this clearly. Claude kept not doing it.
Environment details
- Claude Code CLI
- Models used: claude-sonnet-4-6, claude-opus-4-6
- Multiple sessions with full context resets
- Design doc written mid-session as corrective measure (did not help)
- User corrections escalated from polite to frustrated to profane (did not help)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗