agent-carnet
v0.1.4
Published
File-based markdown notebook CLI for AI agents — the leather notebook in your jacket pocket.
Maintainers
Readme
Agent Carnet (pronounced /ˌeɪdʒənt kɑːrˈneɪ/, like "agent kar-NAY") is a tiny CLI — agent-carnet — that gives AI coding agents (Claude Code, Codex, Cursor) a shared notebook on disk. Each note is a markdown file under .carnet/<category>/<slug>.md.
💡 Why Agent Carnet
Notes are just markdown files. Most agent-memory tools hide notes inside a vector DB or proprietary store, so you cannot grep, git diff, or hand-edit them. Agent Carnet keeps every note as a plain .md file under .carnet/. The agent writes through the CLI; you read and review the exact same files with the tools you already use. Anything that can shell out — Claude Code, Codex, Cursor, your own scripts — works against the same notebook. No SDK, no MCP, no daemon.
Stale notes disappear on their own. A note-store that only grows is a note-store that rots. Every carnet has a 30-day lifespan that resets each time it is read or written, so useful notes survive and the rest drift to .trash/ automatically (with a 7-day grace period before deletion). Pin anything you cannot afford to lose with keep: true. The notebook stays alive without manual cleanup.
🚀 Quick start
# Initialize a notebook in the current directory
npx agent-carnet init
# Save a note (stdin or --body)
echo "Notes about iconv-esm interop." | npx agent-carnet save deps/iconv-issue \
--summary "iconv-esm compatibility fix" \
--agent claude-code \
--tags compat,esm
# Look around
npx agent-carnet list
npx agent-carnet find iconv
npx agent-carnet read deps/iconv-issueOr install once:
npm install -g agent-carnetInstall the Claude Code skill
Agent Carnet ships a bundled skill at skills/agent-carnet/ (SKILL.md plus a small references/ set) so any Claude Code, Codex, or Cursor session knows when to reach for the CLI. Install it with npx skills, the open agent-skills installer:
# Project install (default) — drops the skill into <cwd>/.claude/skills/agent-carnet/
npx skills add yamadashy/agent-carnet
# Global install — drops it into ~/.claude/skills/agent-carnet/ instead
npx skills add yamadashy/agent-carnet -gnpx skills handles the install / uninstall / list lifecycle uniformly across agents, so Agent Carnet itself doesn't need to know about Claude Code's filesystem layout.
🔄 Lifespan
Every carnet flows through this lifecycle. Useful notes get reset by use, idle ones decay to .trash/, and stale ones eventually disappear:
stateDiagram-v2
direction LR
[*] --> Live: save
Live --> Live: read / used / save --update<br/>(resets lifespan)
Live --> Trash: expired (30d idle)
Trash --> Live: restore
Trash --> [*]: delete (7d)expired (30d idle) means last_used + lifespan < today — auto-prune sweeps the carnet to .trash/ once nothing has touched it for the lifespan window (default 30 days). delete (7d) is the hard delete that runs after the trash TTL. The table further down spells out which fields each command on the self-loop actually bumps (read / used / save --update differ on which of updated, last_used, use_count they touch).
The expiry date is computed from two frontmatter fields:
expiry = last_used + lifespanlast_used— last time the carnet was read or applied (YYYY-MM-DD, UTC, CLI-managed). Falls back toupdatedfor legacy carnets that pre-date the field.lifespan— duration string (30d,90d,1y,never). Defaults toAGENT_CARNET_DEFAULT_LIFESPAN(30d) when omitted.
When expiry <= today, the carnet is considered stale and auto-prune moves it to .trash/ on the next CLI invocation.
Four CLI-managed dates / counters
| Field | Bumped by | Purpose |
|---|---|---|
| created | save (first time only) | Birth date. Immutable. |
| updated | save, save --update | Last content modification. Independent of usage. |
| last_used | save, read, used | Last interaction. Drives expiry. |
| use_count | used (only) | Explicit-use counter. A reference signal of importance. |
Refresh-on-use: two strengths of "use"
A carnet's life is extended whenever it is used. The CLI distinguishes two strengths so the same notebook can express "kept alive because it gets read" and "matters enough to be cited as load-bearing":
flowchart TB
A[carnet accessed?] -->|find / list| B[no signal<br/>lifespan unchanged]
A -->|read| C[weak signal<br/>last_used = today]
A -->|used| D[strong signal<br/>last_used = today<br/>use_count + 1]
style B stroke-dasharray: 4 2
style C stroke-width:2px
style D stroke-width:3px- Weak signal —
read. Reading the body resets the lifespan. The agent bothered to pull the carnet into context, which is enough to keep it alive. - Strong signal —
used. After applying a carnet's content (fixing a bug with the recorded fix, citing a debunked hypothesis, reusing a vocabulary entry), callagent-carnet used <path>. This bumpslast_usedand incrementsuse_count— a durable importance metric you can sort by (agent-carnet list --sort use_count) or that downstream tooling can read.
| Action | updated | last_used | use_count |
|---|---|---|---|
| save <path> (create) | today | today | 0 |
| save <path> --update | today | today | unchanged |
| read <path> | — | today | — |
| read <path> --no-touch | — | — | — |
| used <path> | — | today | +1 |
| find <keyword> | — | — | — |
| list, move, rm | — | — | — |
Useful notes survive because they get read. Important notes accumulate use_count and rise to the top of importance-sorted views. Notes nobody touches drift toward expiry on their own — no manual triage required.
Live, trash, and hard delete
The lifecycle diagram above has three states on disk:
- live (
.carnet/<category>/<slug>.md) — the working notebook. - trash (
.carnet/.trash/<category>/<slug>.md) — soft-deleted carnets, kept forAGENT_CARNET_TRASH_TTL(default7d). Original sub-path is preserved, so restoring ismv .carnet/.trash/foo/bar.md .carnet/foo/bar.md. - hard delete — anything in
.trash/whose mtime is older than the trash TTL is unlinked permanently. This is the only step that loses data.
Auto-prune
Every CLI invocation (except --help / --version) sweeps the notebook:
- Walks
.carnet/and moves carnets whereexpiry <= todayto.trash/. - Hard-deletes anything in
.trash/whose mtime is older than the trash TTL.
Opt out per call with --no-auto-prune, or globally with AGENT_CARNET_AUTO_PRUNE=false and run agent-carnet prune --auto from CI instead. The latter is the recommended pattern for shared, git-tracked notebooks: each developer's local CLI should not silently delete other people's carnets.
Exemptions
A carnet never expires when:
keep: trueis set (explicit pin), orlifespan: neveris set, or- both
updatedandlast_usedare missing (treated as "freshly imported, not yet measured" — the safety valve that prevents a one-shotagent-carnet listfrom sweeping unmigrated notes into.trash/).
🍳 Cookbook
Agent Carnet is just a folder of markdown files; useful patterns emerge from how you tag and link them, not from special folders or commands. The example below stays inside the existing CLI surface — only the tags: field carries the convention, so the carnet remains a portable markdown file you can also open in Obsidian, VS Code, or any editor.
Vocabulary alignment
Multiple agents (and humans) routinely invent different names for the same concept — Claude Code calls something "staging adapter", Codex writes "proxy layer", a human's note uses "forward middleware". By the time anyone notices, three identifiers have leaked into the codebase.
Use one carnet per term, tagged with vocab. The tags: value declares membership; the optional meta.vocab.* subtree carries structured data downstream tools can act on (resolve an alias to its canonical, list rejected names, etc.) while the body explains the why in narrative form:
---
summary: "staging adapter — the thin proxy in front of POST /v1/stage"
agent: claude-code
tags: [vocab]
related:
- .carnet/vocab/payload-envelope.md
- src/staging/adapter.ts
meta:
vocab:
canonical: staging adapter
aliases:
- proxy layer
- forward middleware
- request shim
---
# staging adapter
## Definition
The thin proxy that fronts the production gateway and reshapes incoming
requests into the `payload-envelope` format. Nothing more.
## Why this name
"proxy" is overloaded; "middleware" collides with the Express concept.
"staging adapter" leaves no doubt about which layer is meant.The agent-side flow is small. Before naming a new concept, the agent checks whether someone already named it:
agent-carnet find <candidate> --in tagsIf a term wins out, the canonical version is saved once, and every subsequent agent (Claude Code, Codex, Cursor, ...) can find it the same way:
echo "..." | agent-carnet save vocab/staging-adapter \
--summary "staging adapter — the thin proxy in front of POST /v1/stage" \
--agent claude-code \
--tags vocabRefresh-on-use does the rest: synonyms that keep getting cited stay alive, ones that nobody invokes drift to .trash/ automatically. The vocab tag is purely a project-level convention — the file is just markdown, and Agent Carnet itself does not know or care that it represents a term.
Hypothesis ledger
Long debugging sessions keep producing dead-ends — "we tried X and it didn't work because Y" — and the next session (or the next agent) cheerfully retries the same thing. Vector search and CLAUDE.md skim well for "what worked"; they're worse at "what was already tried and ruled out". A small carnet per hypothesis fixes that with no extra machinery: tag it hypothesis, write the test and the verdict in the body, let meta.hypothesis.* carry the structured status:
---
summary: "iconv-lite v0.7 esm import path — types broken upstream"
agent: claude-code
tags: [hypothesis]
related:
- https://github.com/pillarjs/iconv-lite/issues/363
meta:
hypothesis:
status: debunked
last_tested: 2026-04-30
---
## Hypothesis
Switching to esm imports should let us run `iconv-lite` on Node 22
(v0.7 advertises ESM support).
## Tests
1. `npm install [email protected]` → type error (`Cannot find module declaration`).
2. Set `tsconfig.moduleResolution` to `bundler` → same error.
3. Inspected v0.7.1 source → broken `package.json#exports` types.
## Verdict
Pin to `v0.6.3`. The whole v0.7 series is broken upstream (Issue #363).
Wait for v0.8 before retrying.Before exploring a new theory, the agent checks whether anyone has been here before:
agent-carnet find <symptom> --in all
agent-carnet find <library> --in tags # narrow to hypothesis: notesIf a hypothesis is debunked, the body explains why and the agent (or human) moves on without burning the same evidence again. Refresh-on-use turns staleness into signal: a debunked hypothesis nobody has needed to consult in 30 days drops to .trash/, which is the right behavior — by then either the library has moved or the problem isn't recurring. The hypotheses that do keep getting cited are the load-bearing "do not touch" entries.
📖 Reference
Lookup material — every command and flag, the on-disk file format, the directory layout, and the runtime knobs.
Commands
| Command | What it does |
|---|---|
| init [--gitignore] | Create .carnet/ in the current directory. --gitignore adds an entry. |
| save <category>/<slug> --summary <s> --agent <a> [--tags] [--related] [--body or stdin] [--lifespan] [--keep] [--update] | Create or update a carnet. |
| list [category] [--recent N] [--tags a,b] [--expiring 7d] [--sort last_used\|updated\|created\|name\|use_count] | List carnets, grouped by category. |
| find <keyword> [--in summary\|tags\|body\|all] [--category] [--limit N] | Pure-JS search. Default scope is summary. Does not refresh last_used. |
| read <category>/<slug> [--no-touch] [--no-frontmatter] | Print a carnet. Bumps last_used to today (weak use signal); pass --no-touch to peek without leaving fingerprints. |
| used <category>/<slug> | Mark a carnet as applied — bumps last_used and increments use_count. The strong use signal; call after the note actually shaped your work. |
| move <from> <to> [--update] | Move a carnet between categories. Trailing / on <to> keeps the source filename. |
| rm <category>/<slug> [--yes] [--hard] | Delete one carnet. Soft-delete to .trash/ by default; --hard unlinks immediately. |
| prune [--dry-run] [--auto] [--interactive] [--include-trash] | Move expired carnets to .trash/. --interactive prompts per carnet (y/N/q). |
Global flags: --json, --no-color, --no-auto-prune, --quiet, --help, --version.
Per-subcommand help: agent-carnet <command> -h (e.g. agent-carnet save -h) prints the focused help for that command — required arguments, all options, and examples — without touching the filesystem.
Skill installation lives outside the CLI — see Install the Claude Code skill above for the npx skills flow.
Frontmatter schema
---
summary: "iconv-esm compatibility fix" # required (one line)
agent: claude-code # required (claude-code, codex, cursor, human, ...)
created: 2026-05-04 # CLI-managed, immutable after first save
updated: 2026-05-04 # CLI-managed, last content modification (save / save --update)
last_used: 2026-05-04 # CLI-managed, last read/applied (save / read / used) — drives expiry
use_count: 7 # CLI-managed, count of explicit `used` calls (importance signal)
tags: [compat, esm] # optional
related: # optional (paths or other carnets)
- src/core/file/encoding.ts
lifespan: 90d # optional (override default 30d)
keep: true # optional (pin against auto-prune)
meta: # optional, free-form extension namespace
<extension>:
<key>: <value>
---Notes:
createdis immutable after the firstsave. Every other CLI-managed date is recorded separately so you can tell "when was this note last edited" (updated) apart from "when was it last applied" (last_used) and "how often has it been applied" (use_count).- Expiry is driven by
last_used, notupdated. Editing the body is independent of using it. See Lifespan above. lifespanaccepts duration strings (30d,90d,1y) and the literalnever.meta:is a deliberate extension point for tools and conventions that need structured data beyond whattags:andrelated:express. The CLI does not interpretmeta:itself — it preserves the full subtree on every read/write so downstream consumers (an Obsidian plugin, a sibling agent, your own script) can read and act on it. Namespace keys under the convention name (meta.vocab.*,meta.hypothesis.*) so different extensions don't collide.
Storage layout
<cwd>/.carnet/
├── <category>/
│ └── <slug>.md
├── <category>/<sub>/
│ └── <slug>.md
└── .trash/ # safety net for auto-pruned carnets
└── <category>/
└── <slug>.mdPhase 1 stores carnets only under the current working directory. A global ~/.carnet/ and --scope flag may come later.
Configuration
Phase 1 has no config file. Behavior is controlled by environment variables:
| Variable | Default | Purpose |
|---|---|---|
| AGENT_CARNET_AUTO_PRUNE | true | Run lifespan/trash sweep on every CLI invocation. |
| AGENT_CARNET_DEFAULT_LIFESPAN | 30d | Default per-carnet expiry. |
| AGENT_CARNET_TRASH_TTL | 7d | How long .trash/ keeps soft-deleted carnets before hard delete. |
🆚 How it differs from built-in agent memory
| | Vendor-managed agent memory | Agent Carnet |
|---|---|---|
| Agents that can use it | One vendor's tool only | Any (the interface is bash) |
| Storage | Opaque, server- or vendor-managed | Plain markdown files on your disk |
| File-direct edits | Not possible | Encouraged — open in any editor |
| Lifespan enforcement | LLM-judged or none | CLI-enforced (auto-prune to .trash/) |
| Frontmatter validation | n/a | CLI-enforced (summary/agent required) |
Agent Carnet is intentionally less ambitious than vendor memories: it does not try to summarize, embed, or reason about your notes. It is just a tidy, auto-expiring file shelf you can share between agents.
🛠️ Development
npm install
npm run lint # biome + oxlint + tsgo + secretlint
npm run test # vitest
npm run build # tsdown bundleSource layout mirrors pdfvision:
src/
├── bin/agent-carnet.ts # thin entry point
├── cli/ # argv parsing, help, version, stdin
├── core/ # pure-ish logic (no process.exit, no console.log)
├── output/ # human + JSON formatters
└── types/ # shared types📜 License
MIT (c) 2026 Kazuki Yamada
