Git captures what a project is. ltm captures what it ran into: the dead ends, the arguments you had with the model and lost, the constraints that shaped the current code without ever appearing in it. A one-file, two-verb protocol. Self-hosted. Model-agnostic. Open.
$ curl -fsSL https://ltm-cli.dev/install | sh
# ~180 lines of spec. ~1,900 lines of Go. that is the whole thing.
Git has the diffs. ltm has the reasons they look the way they do.
| verb | description | example |
|---|---|---|
| push | Agent emits a Core Memory Packet and pushes it to any ltm-compatible server. | $ ltm push packet.json (or cat … | ltm push -) |
| pull | A new session on any machine pulls it back. | $ ltm pull 01JDMB4W1YNJ… |
HTTPS transport with bearer-token auth, or OAuth 2.0 device-flow against the managed hub. SQLite storage. ULID-keyed packets. Nothing exotic; nothing reinvented.
The CLI wraps those two verbs with ergonomic helpers. Most agents only ever call save — serialize the current session into a packet and push it in one step — and resume on the other end. Run ltm resume with no arguments to get an interactive picker of your recent packets; pick one and its resume block is copied straight to your clipboard, ready to paste into the next session. Pass a packet ID to skip the picker and print the block to stdout. The rest (auth, ls, show, rm, example, update, a self-hostable server, and mcp — a Model Context Protocol stdio server that exposes the same verbs as tools to any MCP-aware agent) are there when you need them. None of them extend the protocol.
Your CLAUDE.md, skills, and tool setup stay yours. Only the intent behind what you are doing travels.
No enterprise tier. No hosted-only features. If it does not run on a $5 VPS, it is not done.
A packet written by Claude is readable by GPT, Gemini, or the next thing.
The protocol is public and versioned. The CLI and server are reference implementations, not the whole product.
Secrets and local state never ride along. The client refuses the push when it finds them, and the author has to opt in out loud.
A packet describes one bump in the road that warranted capture: the goal, the decisions that locked in, the approaches that failed, the next concrete step. Five required fields. Six recommended. Forward-compatible. Every field earns its byte.
1{
2 "ltm_version": "0.2",
3 "id": "01JDMB4W1YNJZQR7K8F3A2H5P9",
4 "created_at": "2026-04-18T22:41:03Z",
5 "provenance": {
6 "agent": "claude-code",
7 "model": "claude-sonnet-4-5",
8 "host": "darwin-arm64"
9 },
10 "goal": "Get llama.cpp inference running on a local GPU for a Rust side-project; benchmark tok/s vs. CPU.",
11 "decisions": [
12 {
13 "what": "Abandon Metal backend on M2",
14 "why": "Kernel compile fails with Xcode 16.2; not worth diagnosing further.",
15 "locked": true
16 },
17 {
18 "what": "Continue on Fedora 40 + RTX 4070 (CUDA 12.4)",
19 "why": "Known-working GPU path; matches target deployment hardware.",
20 "locked": true
21 },
22 {
23 "what": "Pin llama.cpp to b3821",
24 "why": "Newer builds regress on sm_89; b3821 bench-verified.",
25 "locked": false
26 }
27 ],
28 "attempts": [
29 {
30 "tried": "brew install llama.cpp",
31 "outcome": "builds, but MPS allocator OOMs on 13B Q4",
32 "learned": "MPS path has a hard ceiling well below VRAM — not a tuning issue."
33 },
34 {
35 "tried": "GGML_METAL=1 make -j",
36 "outcome": "link error: _ggml_metal_graph_compute undefined",
37 "learned": "Metal symbols moved; would need a Xcode 15 toolchain — out of scope."
38 }
39 ],
40 "open_questions": [
41 "Is flash-attn worth enabling on sm_89 for <14B?",
42 "Does llama-bench reflect real inference? Need an e2e harness."
43 ],
44 "next_step": "On the Fedora box: clone llama.cpp@b3821, build with -DLLAMA_CUDA=ON, run llama-bench -m qwen2.5-7b-q4_k_m.gguf -ngl 99."
45}
Read attempts as "approaches that failed and shaped the current decision," not "things I did." A packet is not a changelog. The 90% of work that went smoothly never needs a packet, because the commits already carry that state. Only the shaped-by-a-constraint stuff needs portable intent, because that is the part a fresh agent cannot reconstruct from the repo.
A protocol whose core promise is "packets travel" has to take seriously what travels with them. Every packet is scanned on the client, before the first network call, and any hit blocks the push. The author can override with --allow-unredacted, but they have to say so out loud.
This is a load-bearing trust property, not a checkbox. Packets are meant to move between machines, teams, and agents; the person writing a packet is not always the person reading it. The scanner is designed to be noisy rather than quiet: block a legitimate push and make the caller opt in, rather than silently leak a credential. Implementation and test vectors live in internal/packet/packet.go (see redactionPatterns). A second implementation of the protocol MUST ship this pre-flight before its first network call.
Install the CLI, grab the bundled example packet onto your clipboard, and paste it into any agent session. The packet is a mid-task snapshot pointing at a small demo repo (github.com/dennisdevulder/ltm-example); a fresh session reads it and picks up the work — no briefing, no ID, no server.
$ curl -fsSL https://ltm-cli.dev/install | sh
$ ltm example --resume
# ✓ Copied the example packet to your clipboard.
# paste into your agent session. it resumes the work mid-thought.
ltm mcp is a Model Context Protocol server over stdio. Register it once and your agent can call save, resume, ls, show, push, pull, rm, example, and whoami as tools. No paste step.
$ claude mcp add ltm -- ltm mcp
# registered. ask your agent to resume. it picks the bundled example
# packet and continues the work, no id needed.
For Cursor, Zed, Claude Desktop, and Continue, drop this into the client's MCP config:
{ "ltm": { "command": "ltm", "args": ["mcp"] } }
$ curl -fsSL https://ltm-cli.dev/install | sh
$ ltm auth https://your-server.dev <token>
$ ltm resume
# arrow keys to pick a packet. block copied to clipboard. paste into any agent.
Self-hosting the server is two commands:
$ ltm server init
$ ltm server --addr :8080
# sqlite on disk. bearer tokens. done.
ltm is written with LLM assistance. A human drives the design, writes the prose, reviews every line, and is accountable for what lands; a coding agent helps with implementation. Commits touched by an agent carry an Assisted-by: trailer naming the tool — the same convention as the Linux kernel's AI Coding Assistants policy. Disclosure, not disguise.
If a PR you send was partly written by an LLM, please do the same: add an Assisted-by: trailer, read the diff as if you had written it yourself, and be the one accountable for it. Details in CONTRIBUTING.md.