Lessons learned

Lessons learned for coding agents: what to write down

Your coding agent fixes a bug, finds a trap, makes a call. The next session starts without any of it, unless someone wrote it down. A lesson is that note. Done well, it gives the agent the memory of a colleague who was there.

This page shows what a useful lesson contains, three real ones from our own work, and how a lesson finds its way back into a session. The format works in a plain Markdown file too.

Five parts of a useful lesson

  1. The symptom, in search words. Write it the way you would describe it before you knew the fix: "deploy hangs", "build green, page dead". Later, someone searches with exactly those words.
  2. The decisive fact first. The number, the address, the command, the file. Put it into the first 100 characters. Briefings and injected notes cut lessons short, and the story fits behind the fact.
  3. What did not work. A dead end costs as much the second time. One line saves the next session from it.
  4. The files. They tie the lesson to the code. An agent about to edit that file can find it.
  5. One command that checks it. A test, a grep, a curl. The next session can confirm in seconds that the lesson still holds.

Keep one fact per lesson. When a lesson turns out wrong, fix that lesson and note why. Two lessons that contradict each other leave the agent guessing.

The format

These are the fields cachly's learn_from_attempts tool writes. Each one answers a question the next session will ask:

topic
A short slug: area, colon, keyword. One topic per fact.
outcome, severity
Did it work? How much does it hurt: critical, major or minor.
what_worked
The fix. The decisive fact goes into the first 100 characters.
what_failed
The dead ends, so the next session skips them.
context
The symptom in the words someone types while still stuck, plus the error message.
file_paths, commands
Where it happened, and the command that shows the fix holds.
tags
Words for filtering.

Three real lessons

All three come from our own work. We translated them, shortened them and removed project names.

1A docs summary invented a field name

The symptom sits in the context, in the words you would search with. The failed path warns the next session off the same shortcut.

{
  "topic": "research:fetch-summary-invents-doc-fields",
  "outcome": "success",
  "severity": "major",
  "what_worked": "LOAD THE DOCS RAW: curl -sL https://code.claude.com/docs/en/hooks.md, then grep. The summary called the UserPromptSubmit stdin field \"user_input\"; the reference says \"prompt\".",
  "what_failed": "Taking field names from a fetch tool's summary of a long page. The summary invented field names and examples, including a matcher for an event that has none. A hook built on it silently does nothing.",
  "context": "Symptom: the docs summary contradicts our own code, which reads payload.prompt. Hook gets no prompt, field missing, undefined.",
  "file_paths": [
    "docs/hooks/userpromptsubmit.tsx"
  ],
  "commands": [
    "curl -sL https://code.claude.com/docs/en/hooks.md -o hooks.md"
  ],
  "tags": [
    "docs",
    "claude-code-hooks",
    "research"
  ]
}

2The image build broke while the tests stayed green

The first version told the story first. The fix started at character 300, after the cut. The rewrite puts the fix first and keeps the story in the other fields.

{
  "topic": "docker:build-broke-on-test-imports",
  "outcome": "success",
  "severity": "major",
  "what_worked": "BUILD WITH tsconfig.build.json THAT EXCLUDES src/**/*.test.ts, and run tsc -p tsconfig.build.json in the Dockerfile. Tests that import ../../app/src break the image build: the image copies only server/src.",
  "what_failed": "Trusting npm test and tsc --noEmit. Both stayed green for 24 hours. Only docker build compiles inside the image, and it ran only at deploy.",
  "context": "Symptom: deploy fails with TS2307 Cannot find module, while tests and type check pass locally.",
  "file_paths": [
    "server/tsconfig.build.json",
    "server/Dockerfile"
  ],
  "commands": [
    "docker build -q -t api:check ."
  ],
  "tags": [
    "docker",
    "typescript",
    "ci"
  ]
}

3A green build hid a dead server

The error text in the context is what someone pastes into a search. The command lets the next session check every file in seconds.

{
  "topic": "nextjs:use-server-exports-only-async-functions",
  "outcome": "success",
  "severity": "critical",
  "what_worked": "A \"use server\" FILE MAY EXPORT ONLY ASYNC FUNCTIONS. Move constants and objects into a plain module and import them from there.",
  "what_failed": "Trusting next build. With an exported const object the build passed, and every page that loaded the file failed at runtime. Only the end-to-end tests caught it.",
  "context": "Next.js 16. Build green, E2E red. Server error: A \"use server\" file can only export async functions, found object.",
  "file_paths": [
    "lib/actions/tasks.ts",
    "lib/tasks/columns.ts"
  ],
  "commands": [
    "grep -rlE '^\"use server\"' lib app | xargs grep -nE '^export (const|let|var|class) '"
  ],
  "tags": [
    "nextjs",
    "server-actions",
    "build"
  ]
}

Lesson 2, before the rewrite

The Docker lesson was first stored with the story up front. This is what a session saw of it before each prompt:

injected
- [major] docker:build-broke-on-test-imports: The server's Docker build was broken for 24 hours and no check caught it: two tests import from ../../app/src, but the i…

The same lesson after the rewrite:

injected
- [major] docker:build-broke-on-test-imports: BUILD WITH tsconfig.build.json THAT EXCLUDES src/**/*.test.ts, and run tsc -p tsconfig.build.json in the Dockerfile. Tes…

The first line tells you something broke. The second tells you what to do.

How a lesson comes back

A lesson helps only when it shows up at the right moment.

  • Before each prompt. A UserPromptSubmit hook searches your lessons for words from the prompt. It puts the best matches next to the prompt, before Claude reads it. cachly's hook adds up to three lessons, one line each: severity, topic and the first 120 characters of what worked.
  • At session start. A SessionStart hook briefs the new session on what matters in this project.
  • On request. The agent searches when it is stuck, with the words of the error in front of it.

The hook searches by words. A lesson that never names its symptom is hard to find for someone who only knows the symptom. That is why part 1 matters as much as part 2.

Which Claude Code hook event fires when

Without a tool: LESSONS.md

You can start today with a file in your repository. Use the same five parts, one heading per fact:

LESSONS.md
# Lessons

One entry per fact. Newest on top. Fix an entry when it turns out wrong.

## docker: build broke on test imports  (major)
- Fact: Build with tsconfig.build.json that excludes src/**/*.test.ts.
- Symptom: deploy fails with TS2307 "Cannot find module"; tests pass locally.
- Did not work: trusting npm test and tsc --noEmit.
- Files: server/tsconfig.build.json, server/Dockerfile
- Check: docker build -q -t api:check .

## nextjs: "use server" exports only async functions  (critical)
- Fact: Move constants and objects out of "use server" files.
- Symptom: build green, every page using the file fails at runtime.
- Did not work: trusting next build.
- Files: lib/actions/*.ts
- Check: grep -nE '^export (const|let|var|class) ' lib/actions/*.ts

Point your agent to it from CLAUDE.md or your editor's rules file. Or load the matching entries before each prompt with a hook: the UserPromptSubmit guide has a working one that reads a notes file.

The file works as long as someone keeps it up to date. The weak spot is the moment right after a fix: the agent moves on, and nobody writes the lesson down.

With cachly

cachly turns those moments into experience without the extra step. Your assistant stores a lesson with learn_from_attempts. When a turn ends with an answer that reports a fix, with words like "fixed" or "root cause", cachly's Stop hook records it on its own. It also learns from your git history.

  • The tool asks for the decisive fact in the first 100 characters, and for the symptom in search words.
  • Updating a lesson needs a one-line reason, like a commit message. The history stays readable.
  • The hooks bring the right lessons back before each prompt.
  • One developer's lesson reaches the whole team.
Claude Code
/plugin marketplace add cachly-dev/cachly-mcp
/plugin install cachly-brain@cachly

Lessons live in your own instance on servers in the EU. What leaves your machine, and where it goes.

Sources

More: Claude Code hooks Β· AI memory in three layers Β· All docs