An agentic coding session is a read-heavy workload wearing a writing-tool costume. Every turn, Claude Code stats and reads dozens to hundreds of files to rebuild context, then runs git status and git diff again to see what it just did — thousands of small syscalls a minute, which is exactly where an SSD is slowest relative to memory.
So I moved the whole workspace into RAM — a tmpfs mount, repos in it, Claude Code launched from there. The read path got dramatically faster. The parts I secretly hoped would get faster did not, and that turned out to be the useful finding.
graph LR
A[GitHub] -->|clone| B[tmpfs /mnt/ram-workspace]
E[SSD checkout] -->|rsync| B
B -->|reads/writes| C[Claude Code session]
C -->|commit + push| A
B -.->|umount or reboot| D[gone]
The arrow that matters is the dotted one. Unmount the tmpfs, or reboot, and everything in it is gone — uncommitted edits and commits you never pushed. That is a real hazard, and it is also the discipline: the only durable state is on the remote, so you push often.
Prerequisites #
- Linux with tmpfs (Ubuntu 20.04+ here),
git, andsudofor the mount - Enough free RAM for the mount plus whatever your test suite allocates — check
free -hbefore picking a size - Working GitHub auth: an SSH key or
gh auth login
macOS has no tmpfs. The nearest equivalent is an hdiutil RAM disk, which I have not tested, so nothing below is claimed for it.
Step 1: Mount the tmpfs, owned by you #
|
|
The uid/gid options are the whole trick. Without them the tmpfs root comes up root:root, and every clone into it fails on permissions — a mistake that reads like a git problem for about ten minutes before you think to check ls -ld on the mount itself.
The size is an upper bound, not an allocation: an 8 GB mount holding 300 MB of repos costs you 300 MB.
Step 2: Get the code in — clone, or rsync from disk #
Two ways to fill the mount, and it is a real choice rather than a right answer. I clone; rsync is the better fit if your SSD checkout is where the work already lives.
| Clone from the remote | rsync from your SSD checkout | |
|---|---|---|
| Starting state | Clean tree, default branch | Exactly what is on disk, half-finished branch and all |
| Uncommitted WIP | Left behind | Comes with you |
| Network | Needs it | Works offline |
| Excludes | Nothing to exclude | An exclude list you have to maintain |
| Remotes | Real origin in every repo |
Whatever the disk copy had |
| Repos not yet on disk | Fine | Impossible |
Clone:
|
|
Verify each clone rather than trusting the loop’s exit status — a clone can land a directory and still leave git unusable, and you want to know that now rather than three turns into a session.
rsync:
|
|
The exclude list is what makes this the harder option to live with. A single tool’s cache or .state directory can run to gigabytes — enough to overflow the mount on its own, before node_modules and build output. Every new cache directory a toolchain invents is a line you have to add, and the failure is a mount that fills mid-session.
rsync also only goes one way, so bringing work back means running it with the paths swapped — same excludes, and --delete now aimed at your SSD checkout, which earns a -n dry run first. And do not try to save space by rsyncing a working tree and attaching a shallow .git to it; the debugging section covers why that produces a repo git cannot read straight.
Keep full history either way. Across my five repos it is ~270 MB, and --depth 1 saved under a second per clone while costing git log and git blame — which the agent uses more than I expected.
Step 3: Put .claude/ at the mount root
#
The easiest thing to forget, and the one with a compounding cost. CLAUDE.md and unscoped .claude/rules/*.md load in full at launch; Claude Code walks up the directory tree from where you started it, so a copy at the mount root is read by every repo below it.
|
|
If your rules live in a repo, clone that repo into the mount root instead. Either way, run /context in a session afterwards — what actually loaded is listed under Memory files, and that is the only check worth trusting.
Skipping it does not fail loudly, it costs you paging. Without the conventions in context the agent derives them by exploring — globbing for patterns, reading neighbours to infer style, finding the test command the hard way — and those reads land in the context window, so compaction arrives sooner. Project-root CLAUDE.md is re-injected from disk after a compact; the exploration it would have replaced is not, so you pay for the same discovery again on the far side. A few kilobytes of rules at the root is the cheapest context you will buy all session.
One thing you do not have to copy: auto memory. It lives in ~/.claude/projects/<project>/memory/ on your real disk, keyed on the git repository rather than the checkout path, so it survives the umount and follows a fresh clone in RAM.
Step 4: Run Claude Code from the mount #
|
|
Step 5: Put a guard in front of the reboot #
This is the part I would not run this setup without. The failure mode is not dramatic — no crash, no error. You reboot on a Tuesday and yesterday’s commits were never pushed.
Save this as ~/bin/ram-check.sh and chmod +x it. It lives on your SSD, not in the mount — a guard that dies with the thing it is guarding is not a guard.
|
|
git log --branches --not --remotes is the important half. git status alone reports a clean tree for commits that exist only locally, which is precisely the work a reboot destroys most quietly.
Scan the workspace root as well as the sub-repos, because the .claude/ edits from Step 3 live there and are the easiest to forget.
Wiring it to something that actually fires #
The exit code is the whole interface: 0 safe, 1 unsaved. Three places to hang it.
By hand, before unmounting. ~/bin/ram-check.sh && sudo umount /mnt/ram-workspace — the && means a dirty workspace stops the unmount.
On the commands you type to shut the machine down. In ~/.bashrc or ~/.zshrc:
|
|
Be honest about the hole this leaves: it catches the words you type into an interactive shell and nothing else. A GNOME shut-down menu, systemctl reboot, an OOM kill, or a power cut all walk straight past it. There is no reliable way to block a shutdown from userspace, so treat every layer here as a reminder, never a lock.
When a Claude Code session ends — the moment you are most likely to close the laptop. In .claude/settings.json at the mount root, which Step 3 just put there:
|
|
SessionEnd fires when the session terminates. It cannot stop the session ending, which is why the payload is a desktop notification rather than a message into a session that is already gone.
Step 6: Cleanup #
|
|
RAM is reclaimed immediately. If the unmount reports the mount busy, some process still holds a file open in it — sudo umount -l defers the teardown until it closes.
What actually got faster #
Measured on my repos, SSD versus tmpfs:
| Operation | SSD | tmpfs | Speedup |
|---|---|---|---|
git status |
150 ms | 10 ms | 15× |
git diff HEAD~3..HEAD |
250 ms | 20 ms | 12× |
| Context load, ~100 files | 300 ms | 8 ms | 37× |
go test ./... |
18 s | 16 s | 1.1× |
The last row is the honest one. Tests are compute-bound, and moving their inputs into memory buys almost nothing — the page cache was already serving those reads on the second run. If your bottleneck is a slow suite, this changes nothing for you.
Where it pays is the tight interactive loop: read files to build context, git-diff after every edit, dozens of times a session. Individually these are 150–300 ms operations, which is why they never show up in a profile. In aggregate, across two hours, they are most of the waiting.
Debugging: three checks that lie to you #
ssh -T git@github.com exits 1 on success. GitHub’s greeting is a successful auth that ends in a non-zero exit, so a script gating on the exit code decides you have no credentials. Match the message instead, and capture before you filter — piping ssh into grep under set -o pipefail fails on ssh’s code regardless of what grep found:
|
|
A missing mountpoint reads as “nothing is mounted.” If mountpoint is absent, mountpoint -q "$RAM_MOUNT" returns 127, which a if ! test happily treats as “not mounted” — and the script then mounts a fresh tmpfs over your live workspace, hiding the unpushed work underneath it rather than refusing. Check the command exists before trusting its answer.
A shallow .git grafted onto a copied working tree produces nonsense. An early version of my script rsynced the working tree and then attached a --depth 1 clone’s .git to it, to save space. The index did not match the tree, so git status reported every file as modified. Cloning shallow with --depth 1 has no such problem; assembling a repo out of two mismatched halves does.
The workflow #
Steps 1–3 go in one ram-setup.sh; Step 5 is ~/bin/ram-check.sh. After that the loop is: run the setup (~45 s), cd in and start Claude Code, push everything you touch, and let the check gate the unmount.
Whether it is worth it depends on the shape of your sessions. If you spend the day in long agentic loops over a large repo, the read path is a real tax and this removes most of it. If you spend the day waiting on a build, buy faster compute instead — this will not help, and I would rather tell you that than sell you the 37× row.