0 min read

How I gave Claude Code a memory, the same way Hermes does

Hermes is an agent that keeps notes on its own work. After each task, it reads its memory files, decides what's worth saving, and writes the answer back so the next session knows a little more than...

Hermes is an agent that keeps notes on its own work. After each task, it reads its memory files, decides what's worth saving, and writes the answer back so the next session knows a little more than the last one.

I wanted that for Claude Code. The catch is that Claude Code doesn't run all day. Each session is a fresh process. The terminal opens, I do some work, the terminal closes, and the process exits. There is no "between sessions" for an always-on agent to write notes during.

So I built the same idea, but using the tools Claude Code already gives you: hooks, slash commands, and the system scheduler your laptop already runs. The result is quieter than Hermes and easier to trust. Here is how it works.

Giving Claude Code a memory: Hermes pattern, Claude Code constraint, and the bridge between them

Why Claude Code needed a different shape

Hermes assumes one long process. It reads its memory files at the start of every turn, and it writes them back at the end. The act of saving is the agent noticing that something was worth saving.

Claude Code is the opposite. Every session is its own short-lived process. Nothing carries over unless I put it on disk. And if I rely on the model to remember to save things, I will lose work, because some sessions end abruptly and the model is busy thinking about the task in front of it, not about journaling.

So I moved the decision out of the model. A small shell script runs every time a session ends, no matter what the model was doing. The script decides if the session is worth keeping. The model only shows up later, when it has time to read what was kept and write it up properly.

The four ideas I kept from Hermes, and the three I added

Hermes has four useful patterns. An identity file the agent reads on every turn. A split between user-facing memory and environment-facing memory. A simple "save or skip" rule the agent applies as it works. And a habit of trimming memory when files get too big.

I kept all four. I also added three things Hermes does not have, because they fit Claude Code better.

From Hermes What I do Why
~/.hermes/SOUL.md ~/.claude/SOUL.md, imported into every session Same idea. Hard cap of 100 lines so the cost stays small.
Two memory files USER.md for me, AGENT.md for the environment Hermes mixes them. I keep them separate.
Save/skip rule Same rule, written into CLAUDE.md Identical.
Trim when big Same, but checked once a day Hermes hopes the agent notices. I just look at the file size.
A hook that runs at the end of every session The model never has to remember.
A daily job run by launchd macOS already runs launchd. I don't have to write a daemon.
Skills only get added after I review them Stops the skill list from filling up with half-baked things.

The hooks are the part that makes everything else work. Without them I would be back to hoping the model remembers.

What the SessionEnd hook actually does

When a Claude Code session ends, it runs a hook. The hook is a 164-line shell script in ~/.claude/hooks/session-end.sh. It always exits cleanly, so it can never block the session from closing. It does four things in order.

First, it checks an allowlist and a denylist. I keep both as plain text files. If the session was running in a folder I have flagged as private, the script exits and forgets the session ever happened.

Second, it counts how much work happened. It uses jq to walk the transcript and count tool calls, edits, and the time between the first and last message. If the session was short and quiet, I do not want it cluttering up my memory. The threshold is ten tool calls, three edits, or ten minutes.

Third, it scans for secrets. It looks at the last 200 entries of the transcript, up to 64 KB, and runs a list of patterns over them. API keys. Private keys. Password lines. Prompt-injection phrases. If anything matches, the queue entry it writes is a stub: no transcript path, no counts, just a note that the session was suppressed.

Fourth, it writes the entry to disk. It writes to a temporary file first and then renames it, so a half-written file can never end up in the queue.

The model is never asked any of these questions. The script asks them with simple counts and regex. That is the part I trust.

The SessionEnd hook: a four-step capture flow (allowlist, threshold, redact, write)

Why I used launchd instead of writing a daemon

Hermes runs all the time, so it has a place to drain its queue. I don't. I could have written a small daemon that polls the queue and runs Claude when something shows up. But a daemon needs restart logic, log rotation, a story for when the laptop sleeps, and a way to know it's healthy.

macOS already has launchd, and launchd already handles all of that. So I gave it two simple plist files.

One runs every day at 09:00. The other runs every Sunday at 09:00. Each one calls a small shell script that checks if there is actually any work to do. If the queue is empty, it exits and nothing happens. If there is work, it pipes a prompt into claude running in headless mode, and the model reads the queue and writes the consolidated notes.

This matters for cost. I already pay for the Max subscription, so calling claude headless is free. The API would charge me per token to do the same thing. For something that runs on a schedule, on a personal laptop, headless Claude Code is much cheaper.

The shell preflight is the part that keeps it cheap. It is pure shell, no model calls. If nothing has changed since yesterday, the daily job costs nothing at all.

Why launchd: daily 09:00 + Sunday weekly schedule, preflight gate, zero cost when idle

Two memory files, not one

Hermes keeps one big memory file. I keep two, because the things I want to remember fall into two clear groups.

USER.md is about me. My role, the words I use, the way I want the agent to write when it writes on my behalf, things I have corrected the agent on. This file changes slowly. I gave it a soft budget of 500 tokens.

AGENT.md is about the world the agent works in. Where jq lives. Which folder the blog pipeline runs from. That the DataCamp solver has a known quirk. This file changes faster, because every new project teaches the agent something new about the environment. I gave it a budget of 800 tokens.

When the daily job processes a session, it reads a short decision tree from ~/.claude/CLAUDE.md:

Is this a fact about me? → USER.md
Is this a fact about the environment? → AGENT.md
Is this a five-step procedure I keep repeating? → propose a skill
Is this a one-off? → drop it

When a file gets close to its budget, the daily job rewrites the file to be denser, and moves the removed text into ~/.claude/memory/.archive/ with the date. Nothing is ever deleted. It is just moved out of the way.

The point of the budget is that I never have to think about it. The file doesn't grow forever. The agent doesn't have to notice the file is too big. The next morning's job notices, and trims.

Two memory files: USER.md for identity, AGENT.md for environment, with budgets and consolidation

Privacy is two layers, and neither of them trusts the model

I do not want secrets ending up in memory files, because memory files are read into every session for a long time. So I check for them twice.

The first check is the regex scan in the hook. It catches the obvious things. Anything that looks like a credential, a private key, a JWT, a password line, or a prompt-injection phrase. The list of patterns lives in the script, and I can add more in ~/.claude/redaction-extras.txt without touching the code. Every match is logged to ~/.claude/learning-queue/.redactions.log, with the pattern that matched but never the content.

The second check is in the prompt I send to the model during the daily job. The prompt tells the model to paraphrase everything and never copy raw transcript text. It lists the things the model must refuse to extract: credentials, tokens, .env contents, financial figures, health data, one-time URLs, pasted private documents. If the model isn't sure, it defers, and the queue entry stays in privacy_deferred status until I look at it.

I use two layers because each one catches what the other misses. The regex has false positives. The model has false negatives. Together they are good enough that I have stopped worrying about it.

Defense in depth: Layer A regex scan in the hook + Layer B paraphrase rule in the daily processor

Skills are different. They get a human review.

Some sessions describe a multi-step procedure I keep doing the same way. When the daily job sees one of those, it does not turn it into a skill on the spot. It writes a proposal to ~/.claude/skill-proposals/ and waits.

The weekly job, on Sunday morning, looks across recent proposals and notices when the same procedure keeps coming up. If three or more candidates point at the same thing, it writes a fuller skill draft. I read it on Sunday or Monday, and decide.

Most proposals get dropped. The few that survive become real skills, and I run /skill-create to add them properly.

I do it this way because skills are loud. Every future Claude Code session sees them in the skill list. Adding a skill is a real commitment, and I don't want the system making that commitment for me.

What I actually see day to day

Four slash commands, and a single line at the top of every session.

/me                  show my identity files with their token counts
/learn-from-queue    drain the queue now instead of waiting for 09:00
/learn-loop-rollback uninstall everything, with confirmation
/learning-loop       show the user guide

All four are marked so the model cannot call them on its own. I have to type them.

The SessionStart hook is seventeen lines. It prints one line at the top of every session, like this:

Learning queue: 1 pending, 1 privacy_deferred, 0 deferred.
Daily processor runs at 09:00 or /learn-from-queue.

If the queue is empty, it says nothing. Everything else is invisible. Sessions end. The next morning, memory files are a little better than they were the day before. I almost never notice it happen.

If you want to build something like this

The files that do the real work are small and easy to read.

  • ~/.claude/hooks/session-end.sh — the capture hook. 164 lines of macOS-friendly bash. No exotic dependencies.
  • ~/.claude/scheduled/capacity-check-daily.sh and daily-learn.md — the shell preflight and the prompt it sends to the model.
  • ~/Library/LaunchAgents/com.anand.claude-learn-daily.plist — the schedule.
  • ~/.claude/CLAUDE.md — the save-or-skip rule the model reads every turn.

The pattern is general. If you have a CLI agent that runs in short sessions, attach a hook to the end of each session, queue work to a folder, and let the system scheduler drain the folder when there is time. The hook is the part that runs every time. The model only shows up when there is something worth thinking about.

Hermes had one good idea, which is that an agent should learn from its own work. The rest is plumbing. I kept the idea, dropped the always-on process, and ended up with something that does the same thing in a way that fits the tool I actually use.

On How I gave Claude Code a memory, the same way Hermes does · 0 comments
Comments are moderated

No comments yet — be the first to add to the discussion. Comments appear after they’re reviewed.

Comments are read before they appear.

Enjoyed this article?

Want more insights?

Subscribe to get the latest articles delivered straight to your inbox.