# claude-cellar
Transparent zstd compression for Claude Code session files via a FUSE filesystem.
[Claude Code](https://claude.com/claude-code) stores every session as a plain
JSONL file under `~/.claude/projects/**/`. Over time these accumulate — hundreds
of MB of text that compress down to ~18% of their size with `zstd -19`.
`claude-cellar` v0.2 mounts a FUSE filesystem on `~/.claude/projects/` so
that Claude sees regular `.jsonl` files while the real bytes live as
`.jsonl.zst` in a separate **store** directory. Decompression happens lazily
on `open()` into a per-FD tmpfs scratch buffer; recompression happens on
the last `close()` if the FD was written to. Multi-claude is correct by
construction (every FD is kernel-isolated; there is exactly one daemon
serving the mount).
Verified compression on real sessions: **~82% size reduction** (zstd -19,
hash-verified round-trip). Decompression on open is sub-second for typical
sessions.
## Install (Linux)
### From crates.io
```bash
cargo install claude-cellar
claude-cellar install # registers and starts the systemd user service
```
That's it. After `install`, `claude` works as always — the FUSE daemon
is up, mounted at `~/.claude/projects/`, and started automatically at
every login.
### From source
```bash
git clone https://github.com/OnCeUponTry/claude-cellar
cd claude-cellar
cargo build --release
cp target/release/claude-cellar ~/.local/bin/
~/.local/bin/claude-cellar install
```
### Migration from v0.1.x
```bash
# If you used v0.1's install (which created a ~/.local/bin/claude shim):
claude-cellar uninstall # removes the v0.1 shim, restores the real claude
# Move existing compressed sessions into the new store layout:
claude-cellar migrate-store
# Install the v0.2 daemon:
claude-cellar install
```
## What you don't have to do
- Run `claude-cellar mount` manually. systemd does it.
- Decompress sessions before resuming. The FUSE mount makes them appear as
regular `.jsonl`.
- Worry about multiple `claude` instances stepping on each other. There is
one daemon, every FD is kernel-isolated.
- Configure anything. Defaults work out of the box.
## Architecture
```
~/.claude/projects/ ← FUSE mount (virtual)
│
▼
[ claude-cellar daemon ]
│ │
decompress on open() re-compress on release()
│ │
▼ ▼
$XDG_RUNTIME_DIR/claude-cellar/ ~/.local/share/claude-cellar/store/
scratch/ (compressed .jsonl.zst)
(per-FD tmpfs)
```
| **Mount** | `~/.claude/projects/` | `CLAUDE_CELLAR_MOUNT_DIR` |
| **Store** | `~/.local/share/claude-cellar/store/` | `CLAUDE_CELLAR_STORE_DIR` |
| **Scratch** (tmpfs) | `$XDG_RUNTIME_DIR/claude-cellar/scratch/` | `CLAUDE_CELLAR_SCRATCH_DIR` |
| **Log** | `$XDG_STATE_HOME/claude-cellar/cellar.log` | — |
| **Config** | `$XDG_CONFIG_HOME/claude-cellar/` | — |
You can put the **store** on any filesystem (local, NFS, encrypted volume).
Scratch should stay on tmpfs (default). The mount is fixed at
`~/.claude/projects/` because that's what Claude looks at.
## Commands
Day-to-day, you run `claude` (not `claude-cellar`). The CLI is here for
maintenance and inspection:
```bash
claude-cellar status # mount state, store size, daemon pid
claude-cellar list <dir> # list sessions in a dir (raw + zst)
claude-cellar log --tail 50 # daemon activity log
claude-cellar mount # manual mount (rarely needed)
claude-cellar umount # manual umount
claude-cellar migrate-store [--from D] # move v0.1 .jsonl.zst into the store
claude-cellar archive <dir> --keep N # batch compress a non-mount dir
claude-cellar compress <file> # one-shot compress (stand-alone)
claude-cellar decompress <file.zst> # one-shot decompress
claude-cellar resume <id> # decompress one session and exec claude --resume
```
## Why FUSE (and not a wrapper)
v0.1 used a `cellar run` wrapper that hydrated every `.jsonl.zst` to
`.jsonl` before exec'ing `claude`, then re-compressed on exit. Two
structural problems:
1. **Transient disk doubled** during a session: ~25 MB of compressed
sessions decompressed to ~125 MB on disk for the duration of every
`claude` invocation.
2. **Multi-claude data loss**: with two `claude` instances open, the
first wrapper to exit ran cleanup over files the others were still
writing.
v0.2 sidesteps both by moving the boundary to the kernel: compressed
files stay compressed in the store; only the *currently open* sessions
are decompressed (in tmpfs); every FD is independent.
## Configuration
All env vars optional. Defaults work out of the box.
| `CLAUDE_CELLAR_STORE_DIR` | Override the store location (default `~/.local/share/claude-cellar/store`). Useful for NFS-shared layouts. |
| `CLAUDE_CELLAR_MOUNT_DIR` | Override the mount location (rare; default `~/.claude/projects`). |
| `CLAUDE_CELLAR_SCRATCH_DIR` | Override the scratch dir (default `$XDG_RUNTIME_DIR/claude-cellar/scratch`). |
| `CLAUDE_CELLAR_MAX_FDS` | Cap on simultaneous open FDs through the FUSE (default 16). |
| `CLAUDE_CELLAR_CLAUDE_BIN` | Explicit path to the real Claude binary (used by `resume`). |
## Compatibility
| Default install (`~/.claude/projects/`) | Works out of the box. |
| `CLAUDE_CODE_SKIP_PROMPT_HISTORY=1` | No `.jsonl` is written; FUSE sees nothing; no-op. |
| `--no-session-persistence` (per-run) | That run does not persist; cellar ignores it. |
| `cleanupPeriodDays` in settings.json | Claude prunes via the FUSE; cellar deletes the matching `.zst` and sidecar. |
## Platform support
- **Linux x86_64 / aarch64**: first-class. v0.2 is the recommended path.
- **macOS / Windows**: not supported in v0.2 (FUSE adapters not yet shipped).
Stay on v0.1.x; it still works for single-instance use.
## Benchmarks
Measured on three real Claude Code sessions (zstd 1.5.7, single archive pass):
| 670 KB | 17.3% | 13.6% | **11.6%** | 11.1% |
| 1.9 MB | 19.5% | 16.9% | **15.0%** | 14.6% |
| 4.6 MB | 23.9% | 20.3% | **18.1%** | 17.8% |
zstd -19 lands within 0.3 points of xz -6 on ratio and decompresses ~20×
faster (important on the `open()` path). zstd -19 is the default.
## License
Licensed under either of:
- **MIT License** ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
- **Apache License, Version 2.0** ([LICENSE-APACHE](LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this work by you, as defined in the Apache-2.0 license, shall
be dual-licensed as above, without any additional terms or conditions.