agent-notepad — persistent, per-objective working memory
What this is
A notepad is one standalone git repo per objective (<group>-<objective>, e.g.
proj-arbbot), holding the agent's working memory for that objective. It survives
context compaction, keeps history (append-only journal, not a rewrite), spans several
code repos from a single session, and syncs across machines. It is the short-term,
auto-loaded tier that complements a curated long-term store — and it evolves and
supersedes handoff-auto: the same continuity machinery,
now objective-scoped instead of cwd-scoped, with history and cross-repo reach.
⚠️ handoff-auto is deliberately UNBACKTICKED and unlinked here: it was REMOVED from this
repo on 2026-09-04, and a backticked name reads to tier-check.py as a reference to a
component this tier ships — which is what the gate caught. The plugin installer still
UNWIRES its old hook entries from an existing settings.json; that is for machines
installed before the removal, and it stays.
Product = a Claude Code plugin: hooks + skills + a notepad template + a small Python
memory adapter. No binary, no daemon. Full rationale in DESIGN.md.
Why it beats naive auto-handoff
handoff-auto keys by cwd → one rewritten file → parallel sessions clobber, no history,
single-repo. A notepad gives zero contention (separate folder + cwd + git repo per
objective), append-only history, and cross-repo context driven from one place via
absolute paths (never cd). The episodic memory index is exercised at both ends by hooks
(write-mirror on Stop, query on digest build), so "memory is actually used" is enforced,
not left to agent discretion.
The four layers
| Layer | Anchored to | Where |
|---|---|---|
| Working memory (Notes) | the task | the notepad repo (NOTES.md + journal) — this skill |
| Per-repo context store | code (file:line) |
<code-repo>/.claude/context/ — df-context-store |
| Episodic index | journals, by prefix | the memory index (MemPalace ref impl) — mirror + digest |
| Curated | distilled cross-project | the long-term store (Engram ref) — existing |
Notepad layout
proj-arbbot/
CLAUDE.md # orientation: objective, repos-in-scope, read-first/dispatch rules
NOTES.md # compact working memory, auto-loaded (≤150 lines, redacted)
SCOPE.md # charter: objective, done-criteria, repo subset
DIGEST.md # standing caveats, hand-maintained, COMMITTED, auto-loaded
repos.manifest.json # the CODE repos this notepad drives
sessions/
index.json # session metadata index
<ISO8601>_<id>.jsonl # append-only journal, one file per session
handoffs/ # deliberate structured handoff docs (/handoff → forces push)
.claude/settings.json # notepad-scoped hooks incl. the commit gate
The hooks (what runs when)
- SessionStart — best-effort
git pull, then FILE-READS-ONLY injectNOTES.md+DIGEST.md+repos.manifest.json(~1–3 s). Outside a notepad, degrades to handoff-auto behavior. The payload has no size ceiling, and this is load-bearing. It is piped tojq -Rs, never passed as an argv element. Until 2026-09-04 it usedjq -n --arg, and Linux caps one argv element at 128 KB (MAX_ARG_STRLEN) while macOS caps only the ~1 MB total — so a 259 KBNOTES.mdrestored fine on the maintainer's laptop and injected zero bytes on every Linux box in the fleet, with the hook still exiting 0. ⚠️ A restore that emits nothing is indistinguishable from a notepad with nothing to say. That is why the encode failure path now injects a WARNING namingNOTES.mdandDIGEST.mdinstead of staying silent: the hook contract is exit 0 always, so the payload is the only channel that reaches the session — stderr is read by nobody. ⚠️ Do not "fix" a largeNOTES.mdby capping the payload here. Bloat is a real and separate problem; capping would restore the silent-truncation failure this removed. - Stop — append deterministic journal entries (files touched, commands, a stop
marker), upsert
sessions/index.json, mirror the journal into the memory index, best-effortgit push. - UserPromptSubmit — soft nudge to keep
NOTES.mdcurrent (backed by the PreCompact floor). - PreCompact — deterministic floor: snapshot recent intent into
NOTES.md+ journal before compaction, so/clearrehydrates losslessly. - PreToolUse(Bash) — the commit gate (ships in the notepad's
.claude/settings.json, arms only in notepad sessions): blocks agentgit -C <code-repo> commits that drift from that repo's df-context-store.
Install
# from the plugin dir; installs to the STABLE path ~/.claude/hooks/agent-notepad/,
# merges the four user-level Notes hooks into ~/.claude/settings.json (idempotent),
# installs this skill, and UNWIRES handoff-auto (files kept — reversible).
plugin/install.sh # targets $HOME
plugin/install.sh --target DIR # targets DIR (used by the test harness against a temp HOME)
The installer backs up settings.json before editing it. To reverse: restore the backup
and re-wire handoff-auto. As a Claude Code plugin, the four Notes hooks are declared in
plugin/.claude-plugin/plugin.json via ${CLAUDE_PLUGIN_ROOT}.
A day in the life
/scope-init proj-arbbot— creates the notepad repo, interviews for objective + in-scope code repos, warm-startsNOTES.md, derives the memory wing (proj).- SessionStart auto-loads
NOTES.md+DIGEST.md; you resume from state instead of re-deriving it. You work across the manifest's repos via absolute paths. - As you go,
NOTES.mdstays fresh (nudged each turn); durable code-anchored learnings go to each repo'sFINDINGS.md/DECISIONS.md(df-context-store), not here. - You
git -C /abs/code-repo commit— the commit gate checks it against that repo's store; a drifting commit is blocked with a fix hint, a compliant one passes. - On Stop, the journal appends + mirrors into the memory index; a background digest build
queries the
projwing so a sibling objective's recent activity shows up inDIGEST.md. - Context fills → PreCompact writes the floor. You
/clearinstead of/compact; the next session rehydrates goal + next-action fromNOTES.mdalone. - At a real milestone,
/handoffwriteshandoffs/<date>-<topic>.mdand forces a push.
Routing (where does this note go?)
Ephemeral task progress → NOTES.md. Durable + code-anchored + single-repo → that repo's
FINDINGS/DECISIONS. Cross-scope episodic (same prefix) → the memory index (mirror +
digest). Deliberate handoff → handoffs/ + remote. Distilled/canonical → the curated store.
Publishing a handoff commits the POINTER with the DOCUMENT
lib/publish-handoff.sh stages NOTES.md and DIGEST.md alongside the handoff file, so
the refreshed Notes land in the same commit. This is not tidiness. SessionStart injects
NOTES.md and only a pointer to the newest handoff — so a cold reader told the Notes
are current has no reason to open the handoff at all. Committing the document without the
pointer keeps the artifact and loses the only route to it, which is exactly what this tier
exists to prevent. Measured twice on 2026-09-04, on two machines, as M NOTES.md left in
the working tree after the publisher had exited 0.
⚠️ It stages those two files and nothing else — deliberately not add -A. A notepad
also holds a manifest, a charter and session journals that other machinery writes on its
own schedule; sweeping them in publishes a half-written record from a different tier under
this one's commit message.
⚠️ The commit is not best-effort; only the push is. A flaky remote must not block a local checkpoint, but a rejected commit reported as success loses the checkpoint entirely — so a non-zero commit (other than "nothing to commit") is surfaced and returns non-zero.
Relationship to handoff-auto
Evolution, not coexistence: the handoff-auto machinery becomes the Notes tier; its hooks are extended and the commit gate is added. The installer unwires handoff-auto (leaving its files in place, reversible). Outside a notepad, behavior degrades to today's.
Non-goals (v1)
No new DB/service (files are truth) · no live context-% trigger · no automatic /clear
(habit) · no shared mutable files · no cross-group auto-sharing · manual human
code-commits are not governed (agent commits only).