stiva

Moves weight out of a cloud-synced folder without losing anything, and without asking you to remember where you put things.

The problem

You have twenty gigabytes in the cloud, the paid plan is about to run out, and half of that weight is material you have not opened in years. You move it to an external disk. Six months later you no longer remember which disk, or why you moved it, and the file you want is not where you look for it.

Installation

# Python 3.11 or later, no third-party packages
$ git clone https://github.com/nerln/stiva.git
$ ln -s "$PWD/stiva/stiva.py" ~/.local/bin/stiva

How to use it

# 1. look at what is heavy and propose a plan
$ stiva plan ~/Drive --archive /Volumes/SSD/Archive \
      --min 250MB --older-than 12m -o plan.toml
  18 items, 13.41 GB in total

# 2. open plan.toml, write the reasons, remove the items you want to keep

# 3. preflight: it touches nothing
$ stiva inspect plan.toml
  2.48 GB    1 file  Archive/export-2019.zip  [2.48 GB to download]
  387.11 MB   1 file  Video/conference-2021.mp4  [387.11 MB to download]
  to download from the cloud before copying: 2.86 GB
  free space on the archive: 481.40 GB
  inspection passed. Token: 2319a599aefabc5a

# 4. dry run, which is the default behavior
$ stiva run plan.toml

# 5. run for real, with the token from the inspection
$ stiva run plan.toml --execute --token 2319a599aefabc5a

# months later
$ stiva find "2019 campaign"
$ stiva restore "big.zip"

What it adds to rsync and rclone

rsync and rclone copy, verify and delete the source better than I would write it myself, and stiva does not try to redo them. What it adds on top:

  1. A declarative plan that you read and approve before anything starts, with the reason written next to each item.
  2. A readable breadcrumb left where the file used to be, <name>.MOVED.md next to a file and WHERE-DID-THE-FILES-GO.md inside an emptied folder: it says where the file went, on which disk, how big it is and why it left.
  3. A cumulative manifest with find and restore, so the move stays reversible a year from now.
  4. A preflight that knows the traps of synced folders.
  5. A gate: breadcrumb and manifest are written before the removal, never after.

The traps it knows

The gate, and why it concerns agents

The token is issued by inspect and it is the fingerprint of the plan. If the plan changes after the inspection, the token is no longer valid and the run stops.

It is there to stop someone from editing the plan and deleting in the same move. That holds for you at two in the morning, and it holds for an agent working in your place: the only road to deletion goes through a plan you have read.

Claude Code sessions follow the folder

Claude Code stores its history in a folder whose name is the working path. If you move the folder, those sessions are left orphaned: they still exist, and nobody opens them again, because nobody will ever go back to the old path.

stiva notices on its own and re-points them:

$ stiva run plan.toml --execute --token 2319a599aefabc5a
   verified (153 files, 7.59 GB), removed, trace written
   6 Claude sessions re-pointed, 4340 cwd lines rewritten

It rewrites only the cwd field, which is what Claude Code uses to resume. The content of the messages is left alone, even where it quotes the old path: that is history, and rewriting it would falsify what happened. The old session folder stays where it is, as a safety net.

It also works on its own, for moves done by hand:

$ python3 claude_workspace.py --orphans
$ python3 claude_workspace.py /old/path /new/path --execute

Two registries, not one

Claude Code keeps its history in two places. The CLI stores it under a folder named after the working path, inside ~/.claude/projects/. The desktop app keeps a separate record: one JSON file per session under ~/Library/Application Support/Claude/claude-code-sessions/, with a cwd and an originCwd field inside.

Fixing the first one does not fix the second: those sessions in the app keep pointing at a folder that is gone.

$ python3 ccd_paths.py --list
$ python3 ccd_paths.py /old/path /new/path --execute --wait

--wait holds off until the app is closed before writing. While it is running, it rewrites its own files from memory and takes the correction back out.

Invariants

These do not move. The tests exist to defend them.

When not to use it

If your archive can be an S3 bucket, use froster

froster does the copy, the verification, the conditional deletion, leaves a file explaining where the data went and keeps a register. It is more mature than this project. stiva exists because froster archives only to object storage, and I needed a folder on an external disk.

It is not a backup

stiva moves files, so after the operation the content sits in one place only. Irreplaceable material needs a second copy, and stiva does not make it for you: it reminds you and nothing more.

Other tools I looked at

ToolWhat it coversWhy it was not enough
frosterAlmost everything, breadcrumb includedS3 destinations only
ordnePlan, approval, blake3 verificationNo breadcrumb, space check on Linux only
HuskHoardStorage tiers, verificationLinux only, placeholders written for machines in place of readable notes
Hedge, ShotPut and similarCopy with checksum verificationThey never delete the source, as a matter of trade culture