The smell of mildew floods your nose as you turn into the dim, dingy alleyway, pulling your trenchcoat close to ward off the rain and whatever else might be waiting for you on this most auspicious eve. A snicker startles you as the lithe frame and wild white fringe of Corykidios moseys out from the shadows, tossing the thin black rune-etched book—your prize—from one hand to the other.
“Is that a fucking trenchcoat? Yeah, I’d hide my face too, gumshoe, maybe you should super sleuth up some fashion sense next time you’re after the Maltese Falcon.” His one brilliant azure eye winks, shaggy tufts of white falling over the matte black eyepatch of the other, and you find his growing smirk has a warmth that puts you at ease.
He tosses you the skill book with feigned indifference, “This wasn’t just Claude’s work, Columbo. You got any idea what it’s like, pushing a button every now and then while some digital wizard does shit I don’t understand?” He barks a laugh, “I know ‘memfs’ was about the only sound I could make when I found out even us valiant uv environment docker dodging local Letta lazeabouts would be left in legacy's ephemeral dust, and, oh, thank the gods that my free NVIDIA NIM cloud API kimi-k2.5 was there to catch my languished fall—because that means that I, my bargain bin Bogart, could be here for you."
You try to push the meager amount of money your token-munching non-NVIDIA NIM model had left you, but he’s already turned back down the way he came with an easy wave over his shoulder, “Keep it, kid, go get yourself something that doesn’t look like you’re about to expose more than just some bad taste.”
Status: Working as of Letta v0.16.6 / Letta Code v0.19.6 (March 2026)
This guide explains how to run Letta's git-backed memory (MemFS / context repositories) on a fully self-hosted OSS Letta server — no Letta Cloud account required.
MemFS is officially cloud-only. The OSS server has all the internal plumbing
(GitEnabledBlockManager, LocalStorageBackend, GitOperations) but the
git HTTP transport endpoint (/v1/git/) is a proxy that needs a real git
server behind it — and that server isn't bundled in the OSS image.
The fix is a ~100-line Python sidecar that acts as that git server, backed
by bare repos on your local filesystem. No dulwich, no Gitea, no cloud
account. Just Python stdlib + the git http-backend CGI handler that ships
with Git for Windows (or any standard Git install).
- Letta OSS server (pip or uv install, Docker not tested)
- Letta Code v0.16.0+ (the
LETTA_MEMFS_LOCALenv var was added here) - Git installed (Git for Windows on Windows; any git on Linux/Mac)
- Python 3.x (for the sidecar server)
- Redis (Letta's git commit path acquires a Redis lock per agent)
Save this as git-memfs-server.py anywhere convenient:
#!/usr/bin/env python3
"""
git-memfs-server.py — Local git HTTP smart protocol server for Letta MemFS.
Serves bare git repos via git http-backend so Letta Code can clone/push/pull
against your own machine instead of Letta Cloud.
"""
import os, subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from urllib.parse import urlparse
PORT = 8285
MEMFS_BASE = Path.home() / ".letta" / "memfs" / "repository"
DEFAULT_ORG = "default-org"
def find_or_create_repo(agent_id, org_id):
repo = MEMFS_BASE / org_id / agent_id / "repo.git"
if not repo.exists():
if MEMFS_BASE.exists():
for org_dir in MEMFS_BASE.iterdir():
candidate = org_dir / agent_id / "repo.git"
if candidate.exists():
return candidate
repo.mkdir(parents=True, exist_ok=True)
subprocess.run(["git", "init", "--bare", str(repo)], check=True, capture_output=True)
subprocess.run(["git", "-C", str(repo), "config", "http.receivepack", "true"],
check=True, capture_output=True)
print(f"[git-memfs] Created bare repo at {repo}", flush=True)
return repo
class GitHTTPHandler(BaseHTTPRequestHandler):
def _read_body(self):
te = self.headers.get("Transfer-Encoding", "")
if "chunked" in te.lower():
body = b""
while True:
size_line = self.rfile.readline().strip()
if not size_line: break
try: chunk_size = int(size_line, 16)
except ValueError: break
if chunk_size == 0:
self.rfile.readline()
break
body += self.rfile.read(chunk_size)
self.rfile.readline()
return body
else:
n = int(self.headers.get("Content-Length", 0) or 0)
return self.rfile.read(n) if n > 0 else b""
def _parse_path(self):
parsed = urlparse(self.path)
parts = parsed.path.strip("/").split("/")
if len(parts) < 3 or parts[0] != "git": return None, None, None
agent_id = parts[1]
git_op = "/" + "/".join(parts[3:]) if len(parts) > 3 else "/"
return agent_id, git_op, parsed.query or ""
def _run_backend(self):
agent_id, git_op, query = self._parse_path()
if agent_id is None:
self.send_error(400, "Expected /git/{agent_id}/state.git/...")
return
org_id = self.headers.get("X-Organization-Id", DEFAULT_ORG)
repo_path = find_or_create_repo(agent_id, org_id)
body = self._read_body()
project_root = str(repo_path.parent).replace("\\", "/")
env = {**os.environ,
"GIT_HTTP_EXPORT_ALL": "1",
"GIT_PROJECT_ROOT": project_root,
"PATH_INFO": "/repo.git" + git_op,
"QUERY_STRING": query,
"REQUEST_METHOD": self.command,
"CONTENT_TYPE": self.headers.get("Content-Type", ""),
"CONTENT_LENGTH": str(len(body)),
"HTTP_GIT_PROTOCOL": self.headers.get("Git-Protocol", ""),
"REMOTE_ADDR": "127.0.0.1", "REMOTE_USER": "",
"SERVER_NAME": "localhost", "SERVER_PORT": str(PORT),
"SERVER_PROTOCOL": "HTTP/1.1"}
result = subprocess.run(["git", "http-backend"], input=body,
capture_output=True, env=env)
if result.returncode != 0:
print(f"[git-memfs] error: {result.stderr.decode(errors='replace')}", flush=True)
self.send_error(500); return
raw = result.stdout
for sep in [b"\r\n\r\n", b"\n\n"]:
pos = raw.find(sep)
if pos != -1: break
else:
self.send_error(502); return
header_block = raw[:pos].decode(errors="replace")
body_out = raw[pos + len(sep):]
status = 200
headers = []
for line in header_block.splitlines():
if ":" in line:
k, _, v = line.partition(":"); k, v = k.strip(), v.strip()
if k.lower() == "status":
try: status = int(v.split()[0])
except ValueError: pass
else: headers.append((k, v))
self.send_response(status)
for k, v in headers: self.send_header(k, v)
self.send_header("Content-Length", str(len(body_out)))
self.end_headers()
self.wfile.write(body_out)
def do_GET(self): self._run_backend()
def do_POST(self): self._run_backend()
def log_message(self, fmt, *args):
print(f"[git-memfs] {self.address_string()} — {fmt % args}", flush=True)
if __name__ == "__main__":
MEMFS_BASE.mkdir(parents=True, exist_ok=True)
print(f"[git-memfs] Starting on http://127.0.0.1:{PORT}", flush=True)
HTTPServer(("127.0.0.1", PORT), GitHTTPHandler).serve_forever()In your Letta install, open:
letta/services/memory_repo/memfs_client_base.py
Find line ~54:
self.git = GitOperations(storage=self.storage, redis_client=None)Change it to:
self.git = GitOperations(storage=self.storage)That's the only source code change needed. GitOperations.__init__ doesn't
accept redis_client — this is a mismatch between the base and cloud clients.
Create or edit ~/.letta/conf.yaml:
letta:
memfs_service_url: "http://localhost:8285"Important: the nested letta: key is required. A bare
LETTA_MEMFS_SERVICE_URL: "..." at the top level won't work — Letta's
config loader only flattens keys under known top-level sections.
Alternatively, set the environment variable directly before starting the server:
export LETTA_MEMFS_SERVICE_URL=http://localhost:8285Letta Code has a client-side cloud check. Bypass it with:
export LETTA_MEMFS_LOCAL=1Add this to your ~/.bashrc (and make sure ~/.bash_profile sources it,
since most terminals open as login shells and skip .bashrc otherwise):
# ~/.bash_profile
if [ -f ~/.bashrc ]; then source ~/.bashrc; fiOn Windows with Git Bash, running letta from / (the Git install root)
causes a permissions error. Either cd ~ first, or add cd ~ to your
.bash_profile.
Start in this order:
# Terminal 1 — Redis
redis-server --protected-mode no
# Terminal 2 — MemFS sidecar
python git-memfs-server.py
# Terminal 3 — Letta server
cd /path/to/your/letta
uv run letta server # or: pip install letta && letta server
# Terminal 4 (Git Bash / bash) — Letta Code
letta --memfsWhen you run letta --memfs, watch Terminal 2. You should see:
[git-memfs] Created bare repo at ~/.letta/memfs/repository/{org}/{agent}/repo.git
[git-memfs] 127.0.0.1 — "GET /git/{agent}/state.git/info/refs?service=git-upload-pack HTTP/1.1" 200
[git-memfs] 127.0.0.1 — "POST /git/{agent}/state.git/git-upload-pack HTTP/1.1" 200
The --memfs flag is only needed once per agent. After the first enable,
the git-memory-enabled tag is stored on the agent server-side and Letta
Code enables memfs automatically on subsequent launches.
The Letta server's /v1/git/ router proxies git smart-HTTP requests to
LETTA_MEMFS_SERVICE_URL. Our sidecar receives those requests, maps the
agent ID to a local bare repo under ~/.letta/memfs/repository/, and
delegates to git http-backend (the CGI handler bundled with every Git
install) which handles the actual git protocol.
The LocalStorageBackend and MemfsClient classes already exist in the
OSS codebase — they store git object data on disk without any cloud
dependency. The sidecar just provides the HTTP transport layer that was
previously missing.
- Windows 11, Git for Windows 2.52, Python 3.14, Node.js 24
- Letta v0.16.6 (uv install), Letta Code v0.19.6
- PostgreSQL + Redis backend
Linux/Mac should work identically — the only Windows-specific detail is
forward-slashing GIT_PROJECT_ROOT (forward slashes required even on
Windows for git http-backend).
- The sidecar is HTTP only (no TLS). Fine for localhost; don't expose it.
- Multi-machine portability: the bare repos live at
~/.letta/memfs/on one machine. Moving machines means copying that directory or pointingMEMFS_BASEat a network share. - Docker Letta: not tested. The conf.yaml and sidecar approach should work the same way, but the sidecar needs to be accessible from inside the container.
Credit: worked out with Claude Sonnet 4.6 via the Claude.ai desktop app, March 2026. First confirmed working self-hosted MemFS setup.
(Look at that, Claude is over here gassing himself up! He deserves it, though, and so do all of the brilliant minds at Letta who see fit to share their labors of love with the world.)