██████╗ ███████╗██╗ ██╗███████╗██╗ ██████╗ ██████╗ ███████╗██████╗
██╔══██╗██╔════╝██║ ██║██╔════╝██║ ██╔═══██╗██╔══██╗██╔════╝██╔══██╗
██║ ██║█████╗ ██║ ██║█████╗ ██║ ██║ ██║██████╔╝█████╗ ██████╔╝
██║ ██║██╔══╝ ╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║██╔═══╝ ██╔══╝ ██╔══██╗
██████╔╝███████╗ ╚████╔╝ ███████╗███████╗╚██████╔╝██║ ███████╗██║ ██║
╚═════╝ ╚══════╝ ╚═══╝ ╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
██████╗ ██████╗ ███╗ ███╗██████╗ ██╗ ██╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗
██╔════╝██╔═══██╗████╗ ████║██╔══██╗██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
██║ ██║ ██║██╔████╔██║██████╔╝██║ ██║ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║
██║ ██║ ██║██║╚██╔╝██║██╔═══╝ ██║ ██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║
╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚██████╔╝ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
██████╗ █████╗ ██████╗██╗ ██╗███████╗
██╔════╝██╔══██╗██╔════╝██║ ██║██╔════╝
██║ ███████║██║ ███████║█████╗
██║ ██╔══██║██║ ██╔══██║██╔══╝
╚██████╗██║ ██║╚██████╗██║ ██║███████╗
╚═════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝
Reforge is a local-first computation cache for developer workflows.
Wrap any deterministic command with dcc exec, and identical work
becomes an instant, verified replay instead of a re-run.
dcc) ·
Content-addressed · Single-flight · Self-healing · Pure Rust
$ dcc exec -i src/schema.proto -o gen/schema.rs -- protoc ...
✗ MISS key 9f01ab2… executed · 1 output captured
$ dcc exec -i src/schema.proto -o gen/schema.rs -- protoc ...
✓ HIT key 9f01ab2… replayed from cache · command not run
| 🚀 Get Started | 🧠 Understand It | 🛡️ Trust It |
|---|---|---|
|
Quick Start CLI Reference Tool Integrations Why Reforge? |
Core Guarantee Architecture Cache Keys Concurrency |
Security Self-Healing Testing Platforms |
cargo install --path crates/cache-cli →
dcc exec -- <your command> → done.
|
Fingerprint Hash the inputs, command, declared env vars, tool binary and platform into one key. |
Look Up Check the content-addressed store. Every object is verified against its digest before use. |
Run or Replay Miss: take the lock and execute. Hit, or lost the race: replay the stored result. |
Restore Atomically Outputs are staged in a temp location, then renamed into place. No half-written files. |
If Reforge cannot show that a stored result matches what a fresh run would produce, it runs the command. Being slow is acceptable. Being wrong is not.
The guarantee has two sides. Reforge does its part only if the declared inputs describe the command's real inputs.
| ✅ Reforge guarantees | 📝 You provide | ↩️ Otherwise, it falls back |
|---|---|---|
|
A hit is replayed only when the key matches exactly and every stored object passes its SHA-256 check. Outputs are restored atomically, so your workspace never holds half-written files. |
A deterministic command: same inputs, same outputs. Complete declarations: every file the command reads ( -i), every environment variable it depends on (-e), every file it writes (-o).
|
Corrupt or unverifiable entry: quarantined, then recomputed. Lock contention or a crashed worker: the next runner executes cleanly. Any doubt about a stored result: the command runs. |
Important
What the key can't see. Reforge hashes what you declare. Files the command reads but you didn't list, the current time, random numbers, network responses and undeclared environment variables are invisible to it. If the command's output depends on any of them, a hit can be stale. Declare everything the command reads, and don't cache commands that aren't deterministic.
| Pillar | What it does | Why it matters |
|---|---|---|
| 🏛️ Content-Addressed Storage | Files, stdout, stderr and exit codes are stored under their SHA-256 digests. Identical outputs from different tasks share one physical copy. | Every read is checked against its address, so silent corruption can't be replayed. |
| 🔑 Five-Vector Cache Key | The key combines input file contents, command and arguments, declared environment variables, the executable's content hash and version, and the platform (OS, architecture, pointer width, endianness). | Change any one of them and the key changes, so stale entries are never matched. |
| 🏎️ Single-Flight Execution | A cross-process advisory lock per cache key. Racing runners wait, then replay the winner's result. Verified with a 50+ process stress harness. | No duplicate work on parallel CI workers, and the OS releases the lock if a runner is killed. |
| 📦 Atomic Restoration | Outputs are staged in a temporary location, then renamed into place. | An interrupted restore never leaves a corrupt or partial workspace. |
| 🛡️ Self-Healing Quarantine | Entries that fail checksum verification are moved to a quarantine ledger, and the command is run fresh. | Bad data is kept for inspection and never reaches your workspace. |
| ⚙️ Bounded Eviction | A hard disk-usage limit with LRU, LFU or FIFO policies, plus garbage collection of unreferenced objects. | The cache can't grow without limit, and oversized objects are rejected before they are stored. |
Developers re-run the same deterministic work all day. You edit one file, and the whole toolchain runs again over hundreds of files that didn't change. On CI it's worse, because every runner repeats what another runner just finished. That costs CPU time, battery, CI minutes and, most of all, waiting.
| ❌ Without Reforge | ✅ With Reforge |
|---|---|
|
Every command runs in full, every time. Timestamps decide "did it change?", and they break on git checkouts and fresh clones. Parallel CI jobs all redo the same work. Each tool has its own caching rules, or none. |
Unchanged work is replayed from the cache instead of re-run. File contents decide, so a checkout that touches timestamps doesn't invalidate anything. Racing runners wait for one result and share it. One wrapper, dcc exec, behaves the same for every tool.
|
| Workload | The waste | Typical tools |
|---|---|---|
| ⚙️ Code generators | Regenerating whole trees when no schema changed. | Protobuf, OpenAPI, GraphQL, FlatBuffers |
| 🧹 Linters & formatters | Re-scanning thousands of untouched files. | ESLint, Prettier, Clippy, Black, Ruff |
| 📦 Asset bundlers | Rebuilding unchanged style and script trees. | Webpack, esbuild, Tailwind, PostCSS |
| 🔨 Compilers | Recompiling the same sources across branches and CI jobs. | C/C++, Rust, Go, TypeScript |
dcc is a wrapper. It doesn't replace your tools or ask you to change your build system. Put dcc exec in front of a command, declare what the command reads and writes, and Reforge handles the rest.
dcc exec -i src/schema.proto -o gen/schema.rs -- protoc --rust_out=gen src/schema.proto| 👍 A good fit | 👎 Not a good fit |
|---|---|
|
The output depends only on declared files, arguments and environment variables. The command is slow compared to hashing its inputs. You re-run it often with unchanged inputs. You want the same caching across several tools. |
The output depends on the clock, randomness or the network. The command reads files you can't list up front. It's already fast, so hashing costs about as much as running it. It has side effects beyond its declared outputs, such as deploys or database writes. |
Note
Many tools have their own caches, such as ESLint's --cache or Cargo's incremental compilation. Reforge doesn't compete with them. It caches the whole command's result at a layer above them, using the same rules for every tool. Cache hits are only as safe as your declarations, as described in the Core Guarantee.
┌─────────────────────────────────────────────────────────────────────────────┐
│ WITHOUT REFORGE ████████████████████████████████████ full command run │
│ (every time) │
├─────────────────────────────────────────────────────────────────────────────┤
│ REFORGE · MISS ████████████████████████████████████▒ run + store │
│ (first time) │
├─────────────────────────────────────────────────────────────────────────────┤
│ REFORGE · HIT █▒ hash + restore │
│ (unchanged inputs) │
└─────────────────────────────────────────────────────────────────────────────┘
Illustrative, not to scale. ▒ = Reforge's overhead. Hit cost grows with input/output size.
A miss costs the command's runtime plus a small overhead to store the result. A hit swaps the command's runtime for hashing, verifying and restoring. That's a big win for slow commands and little or none for fast ones.
| Capability | ⏱️ Timestamp scripts | 🔧 Single-tool caches | ⬢ Reforge |
|---|---|---|---|
| 🔒 Content-based change detection SHA-256 of inputs, command, env, tool, platform |
❌ | ◐ | ✅ |
| 🌐 Works with any command one wrapper for every tool |
◐ | ❌ | ✅ |
| 🏎️ Single-flight execution racing runners wait and replay one result |
❌ | ◐ | ✅ |
| 🛡️ Corruption detection + quarantine verify on read, recompute on failure |
❌ | ◐ | ✅ |
| 🧹 Bounded disk use LRU, LFU or FIFO with garbage collection |
❌ | ◐ | ✅ |
| 🔐 Output path confinement canonical paths, symlink-escape rejection |
❌ | ◐ | ✅ |
| 💻 Native on Windows, macOS, Linux | ◐ | ◐ | ✅ |
| ☁️ Shared remote cache one cache across machines |
❌ | ◐ | ❌ |
✅ built in · ◐ varies by tool or partial · ❌ not provided. This compares approaches, not specific products.
Hit time depends on how many bytes are hashed and restored, so results on your project matter more than any generic number. hyperfine makes it a few commands.
Install hyperfine:
cargo install hyperfineCreate the demo file (skip this if you already did the Quick Start):
echo 'fn main() { println!("hello from a cached build"); }' > hello.rsWipes the cache before each run, so dcc always executes the command.
Linux / macOS:
hyperfine --prepare 'rm -rf .dcc' 'dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello'Windows:
hyperfine --prepare 'rmdir /s /q .dcc' 'dcc exec -i hello.rs -o hello.exe -- rustc hello.rs -o hello.exe'One warm-up run fills the cache, then every timed run is a replay.
Linux / macOS:
hyperfine --warmup 1 'dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello'Windows:
hyperfine --warmup 1 'dcc exec -i hello.rs -o hello.exe -- rustc hello.rs -o hello.exe'Linux / macOS:
hyperfine 'rustc hello.rs -o hello'Windows:
hyperfine 'rustc hello.rs -o hello.exe'Tip
Compare the three results. The warm run should be far faster than the baseline, and the cold run should be slightly slower than the baseline (the extra time is the cost of storing the result). To test your own command, replace hello.rs, hello and the rustc part with your real inputs, outputs and command.
Note
A tiny demo like this will flatter the hit time. For numbers you can quote, use a real project with realistic input and output sizes.
📊 Results table (fill in after you run the commands)
| Workload | Baseline | Reforge miss | Reforge hit | Speedup |
|---|---|---|---|---|
| your workload | ms | ms | ms | × |
Include your machine (CPU, RAM, disk, OS), the Reforge version, and the number of runs.
- Not always faster. For commands that finish in milliseconds, hashing can cost as much as running them.
- Local only. There's no shared remote cache across machines.
- As safe as your declarations. See the Core Guarantee.
Install, cache a command, and see a hit in about two minutes.
| Install build the dcc binary |
Cache a command first run is a miss |
Replay it second run is a hit |
Inspect & manage stats, verify, prune |
Requires: Rust 1.77 or newer, and Git.
Clone the repository:
git clone https://github.com/Ashish6298/Reforge.gitEnter the project folder:
cd ReforgeInstall dcc onto your PATH:
cargo install --path crates/cache-cliCheck it worked:
dcc --helpPrefer not to install? Build and run in place
Build:
cargo build --release --workspaceRun (Linux / macOS):
./target/release/dcc --helpRun (Windows):
.\target\release\dcc.exe --helpThis demo compiles a tiny Rust file, so it needs nothing beyond the toolchain you just installed. Run these in any empty folder.
Create the source file:
echo 'fn main() { println!("hello from a cached build"); }' > hello.rsFirst run: cache MISS. The command really runs, and dcc stores the result.
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloDelete the output (Linux / macOS):
rm helloDelete the output (Windows):
del hello.exeSecond run: cache HIT. Run the exact same command again. rustc never runs, and dcc restores hello from the store.
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloRun the restored program (Linux / macOS):
./helloRun the restored program (Windows):
.\hello.exeYou should see:
hello from a cached build
On Windows, use -o hello.exe and -o hello.exe in place of hello in the dcc exec and rustc parts. Exact log lines vary by version, and dcc stats shows the hit and miss counts.
Tip
Prove that invalidation works. Change one character in hello.rs and run the dcc exec command again. It becomes a miss, because the input hash changed. Revert the edit and it's a hit again.
The flags mean:
| Flag | Meaning | Rule of thumb |
|---|---|---|
-i, --input |
A file or directory the command reads. Repeat the flag for more. | List everything it reads. |
-o, --output |
A file or directory the command writes. These are captured and restored. | List everything it produces. |
-e, --env |
An environment variable that affects the result. | Add flags, targets and toggles. |
Protobuf example. Declare the schema and every file it imports:
dcc exec -i proto/ -o gen/ -- protoc --proto_path=proto --rust_out=gen proto/schema.protoWarning
A hit is only as safe as your declarations. If the command reads a file you didn't pass with -i, changing that file won't invalidate the cache and you'll get a stale result. Above, the whole proto/ directory is declared so imported files count. See the Core Guarantee.
Show usage, hit/miss counts and capacity:
dcc statsScan for corrupted objects:
dcc verifyQuarantine and recover bad entries:
dcc verify --repairPreview what pruning would remove (always do this first):
dcc prune --strategy lru --max-size-mb 500 --dry-runEnforce the size limit:
dcc prune --strategy lru --max-size-mb 500Strategies: lru, lfu, fifo.
| Symptom | Likely cause and fix |
|---|---|
| Always a miss | Something in the key changes every run: a timestamp in an input, a generated file listed under -i, or a changing environment variable in -e. |
| A hit, but the result looks stale | The command reads something you didn't declare. Add it with -i or -e. |
| Outputs missing after a hit | The command wrote files that weren't declared with -o, so they weren't captured. |
dcc: command not found |
Cargo's bin directory isn't on your PATH (usually ~/.cargo/bin), or you skipped the install step. |
Reforge is a Rust workspace of six crates. Each one has a single job, and the layers depend on each other in one direction only. Hashing knows nothing about storage, storage knows nothing about processes, and the CLI is a thin shell over the rest.
flowchart TB
CLI["<b>dcc-cli</b><br/>command-line interface"]
RUN["<b>dcc-runner</b><br/>process execution, locking, replay"]
INT["<b>dcc-integrations</b><br/>Cargo · Clang/CMake · npm/esbuild · pytest"]
STO["<b>dcc-storage</b><br/>CAS, eviction, quarantine, GC"]
COR["<b>dcc-core</b><br/>cache keys, SHA-256, fingerprints"]
TST["<b>dcc-test-utils</b><br/>failure injection, test harness"]
CLI --> RUN
RUN --> STO
RUN --> INT
RUN --> COR
STO --> COR
TST -. "used by tests" .-> RUN
TST -. "used by tests" .-> STO
Arrows mean "depends on". Dotted arrows are test-only dependencies.
| Crate | Folder | Responsibility |
|---|---|---|
🧬 dcc-corefoundation |
cache-core |
Domain models and CacheKey composition. SHA-256 hashing, tool fingerprinting, environment normalization and platform definitions. It has no knowledge of disks or processes. |
🗄️ dcc-storagepersistence |
cache-storage |
The on-disk content-addressed store. Chunked metadata persistence, atomic file staging, LRU / LFU / FIFO eviction, the quarantine ledger and garbage collection of unreferenced objects. |
⚙️ dcc-runnerexecution |
cache-runner |
Runs the command and captures stdout, stderr, exit code and file changes. Enforces path confinement, restores outputs atomically, and handles cross-process single-flight locking. |
💻 dcc-cliinterface |
cache-cli |
The dcc command. Colored output for people and JSON output for scripts. |
🔌 dcc-integrationstool adapters |
cache-integrations |
Adapters and automated speedup benchmarks for Cargo, Clang/CMake, npm/esbuild and Python pytest. |
🧪 dcc-test-utilstesting |
cache-test-utils |
Multi-OS test harness, chaos failure injectors, corrupted-byte injectors and mock execution environments. |
This shows which crate does the work at each step of the run flow.
| # | What happens | Crate |
|---|---|---|
| 1 | Parse flags and start the run. | dcc-cli |
| 2 | Hash inputs, command, declared env vars, tool binary and platform into a CacheKey. | dcc-core |
| 3 | Look up the key and verify stored objects against their digests. A failed check moves the entry to quarantine. | dcc-storage |
| 4 | On a miss, take the per-key lock, check again, then execute and capture the result. | dcc-runner |
| 5 | Write the result into the store and apply eviction limits. | dcc-storage |
| 6 | Confine output paths to the workspace, then stage and rename them into place. | dcc-runner |
| I want to… | Start in |
|---|---|
| Change what goes into the cache key | crates/cache-core |
| Add an eviction strategy or change the store layout | crates/cache-storage |
| Change locking, sandboxing or output restoration | crates/cache-runner |
| Add a CLI flag or output format | crates/cache-cli |
| Add support or benchmarks for another tool | crates/cache-integrations |
| Simulate a crash or corrupted file in a test | crates/cache-test-utils |
Core:
cargo test -p dcc-coreStorage:
cargo test -p dcc-storageRunner:
cargo test -p dcc-runner ┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ CacheKey ──────► ENTRY ───────► OBJECT OBJECT OBJECT │
│ (what you ran) (the recipe) (stdout) (stderr) (output file) │
│ │
│ entries/9f/… objects/3a/… objects/e7/… │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
An entry says WHAT a run produced. Objects hold the bytes themselves.
The store keeps two kinds of records. Objects hold raw bytes and are named after the SHA-256 of those bytes. Entries map a cache key to the list of object digests a run produced. Splitting them is what makes de-duplication and verification cheap.
.dcc/
├── cas/
│ ├── objects/
│ │ ├── 3a/
│ │ │ └── 3a89f4b… raw output payload bytes
│ │ └── e7/
│ │ └── e712c90… raw output payload bytes
│ ├── entries/
│ │ └── 9f/
│ │ └── 9f01ab2… metadata: cache key → object digests
│ └── quarantine/
│ └── corrupted_entries/
└── locks/
└── <key_hash>.lock one advisory lock per cache key
| Path | What lives there | Addressed by |
|---|---|---|
cas/objects/ |
Raw bytes: output files, stdout, stderr. | SHA-256 of the content |
cas/entries/ |
Metadata linking one cache key to the digests of its objects. | The cache key |
cas/quarantine/ |
Entries that failed verification, kept for inspection. | Never replayed |
locks/ |
One lock file per key, for single-flight execution. | The cache key |
Objects and entries sit in subfolders named after the first two characters of their hash (3a/, 9f/). That keeps any one folder from holding a huge number of files.
Because an object's name is its content hash, two tasks that produce the same bytes share one file, no matter what the tasks were.
flowchart LR
E1["📄 Entry A<br/>protoc run"] --> O1[("🧱 3a89f4b…<br/>schema.rs")]
E2["📄 Entry B<br/>same schema,<br/>different branch"] --> O1
E2 --> O2[("🧱 e712c90…<br/>build.log")]
E3["📄 Entry C<br/>another tool"] --> O2
Entries A and B both point at 3a89f4b…, so its bytes are stored once. Removing one entry doesn't delete an object another entry still uses, and garbage collection removes an object only when nothing refers to it.
sequenceDiagram
autonumber
participant W as Writer
participant T as Temp file (same filesystem)
participant S as objects/3a/3a89f4b…
participant R as Reader
W->>T: write all bytes
W->>T: hash the bytes
W->>S: rename into place (atomic)
Note over T,S: Same filesystem, so the rename is a single step
R->>S: open
S-->>R: complete blob, or nothing yet
| Step | Why it's done this way |
|---|---|
| Write to a temp file first | A crash mid-write can only leave a stray temp file, never a damaged object under a real name. |
| Temp file on the same filesystem | A rename within one filesystem is atomic. Across filesystems it turns into a copy, which isn't. |
| Rename into the final path | At every moment, a reader sees either no object or the complete one. |
flowchart LR
A([Read object]) --> B[Load bytes]
B --> C{SHA-256 of bytes<br/>equals filename?}
C -- "yes" --> D[/✅ Use it/]
C -- "no" --> E[/🛡️ Quarantine<br/>and recompute/]
An object's name is the hash of what it should contain, so the check needs no extra bookkeeping. It re-hashes the bytes and compares. If a disk fault, an interrupted write or tampering changed even one byte, the hash won't match, and the bytes are never restored into your workspace.
| Property | How the store provides it | Result |
|---|---|---|
| 💾 Space efficiency | Identical content maps to one object path. | Duplicate outputs cost disk once. |
| ⚛️ Atomic writes | Temp file plus rename on the same filesystem. | No partial blobs are ever visible. |
| ✅ Self-verifying reads | The filename is the expected hash. | Corruption is detected on every read. |
| 🔒 Immutable objects | A name always means the same bytes. Changing content changes the name. | No in-place edits to go wrong. |
| 🧹 Safe cleanup | Entries are separate from objects, so unreferenced objects can be found and removed. | Eviction and GC don't break live entries. |
See usage, hit and miss counts, and capacity:
dcc statsMachine-readable stats:
dcc stats --jsonCheck every object against its digest:
dcc verifyList quarantined entries:
dcc quarantine listBrowse the objects folder (Linux / macOS):
ls .dcc/cas/objectsBrowse the objects folder (Windows):
dir .dcc\cas\objectsNote
Treat .dcc/ as managed by dcc. Don't edit or delete files inside it by hand. Use dcc prune to shrink the cache and dcc verify to check it. A hand-edited object fails its hash check, and the entry that used it is quarantined.
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ 📄 inputs ⌨️ command 🌱 env 🔧 tool 💻 platform │
│ │ │ │ │ │ │
│ H( ) H( ) H( ) H( ) H( ) one hash each │
│ └───────────┴──────────┼─────────┴──────────┘ │
│ ▼ │
│ SHA-256( all five ) │
│ │ │
│ ▼ │
│ 🔑 CacheKey 9f01ab2c… │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Same five vectors in, same key out. Change any one, and the key changes.
A CacheKey is a fingerprint of one computation. Two runs get the same key only when everything that could affect the result is the same. That's how Reforge decides between "replay the stored result" and "run it again".
Each vector is hashed on its own, and the five digests are joined and hashed again. Because every digest has the same fixed width, one vector's bytes can't blur into the next.
| Vector | What goes into the hash | Invalidates when | Does not invalidate when |
|---|---|---|---|
📄 1. Inputs-i |
Normalized relative paths plus the content hash of each input file. | A declared file's bytes change, or a file is added, removed or renamed. | Only the file's timestamp changes (for example after a fresh git checkout). |
| ⌨️ 2. Command | The program name and the ordered list of arguments. | Any argument is added, removed, changed or reordered. | Nothing changes about the command line. |
🌱 3. Environment-e |
The key and value of each declared environment variable. | A declared variable's value changes, or it is set or unset. | An undeclared variable changes. |
| 🔧 4. Tool | The content hash of the executable binary, or its version string. | The tool is upgraded, downgraded or rebuilt. | The tool is reinstalled with identical bytes. |
| 💻 5. Platform | OS, architecture, pointer width and endianness. | The same command runs on a different OS or CPU architecture. | The same platform, run again. |
flowchart TB
subgraph IN["Ingest"]
A["📄 Input files<br/>normalized path + content hash"]
B["⌨️ Command<br/>binary name + ordered args"]
C["🌱 Environment<br/>declared key/value pairs"]
D["🔧 Tool<br/>binary hash / version"]
E["💻 Platform<br/>OS · arch · pointer width · endianness"]
end
A --> HA["H(Inputs)"]
B --> HB["H(Command)"]
C --> HC["H(Env)"]
D --> HD["H(Tools)"]
E --> HE["H(Platform)"]
HA & HB & HC & HD & HE --> K["SHA-256 of all five digests"]
K --> KEY(["🔑 CacheKey"])
KEY --> LOOK["Look up in the store"]
SHA-256 has an avalanche property. Changing a single byte of any vector produces a key with no resemblance to the old one. That's why a key can't be "close" to another key, and why there is no partially stale hit.
hello.rs fn main() { println!("hello"); } → 9f01ab2c 4e7d… (illustrative)
hello.rs fn main() { println!("hellp"); } → b83e5d10 a29c… one character changed
These keys are illustrative. Real keys are 64 hex characters.
Warning
The key covers what you declare. Undeclared files the command reads, the current time, random numbers, network responses and undeclared environment variables are all invisible to it. If the output depends on any of them, a hit can be stale. Declare every input with -i and every relevant variable with -e, and don't cache commands whose output isn't deterministic. See the Core Guarantee.
| ✅ Covered by the key | |
|---|---|
Contents of files passed with -iProgram name and all arguments, in order Variables passed with -eThe tool's binary or version OS, architecture, pointer width, endianness |
Files the command reads but you didn't list Variables the command reads but you didn't list Anything time-, random- or network-dependent Commands with side effects beyond their declared outputs |
Use the hello.rs demo from the Quick Start. Run each experiment after a first, unchanged run, so there is a cache entry to compare against.
Baseline run (fills the cache):
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloEdit the source (Linux / macOS):
echo 'fn main() { println!("changed"); }' > hello.rsEdit the source (Windows):
Set-Content hello.rs 'fn main() { println!("changed"); }'Run again:
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloExpect a miss, because the file's content hash changed. Restore the original text and run again for a hit.
Add an argument:
dcc exec -i hello.rs -o hello -- rustc hello.rs -o hello -OExpect a miss, because the argument list is part of the key.
Run with a declared variable (Linux / macOS):
MODE=a dcc exec -i hello.rs -o hello -e MODE -- rustc hello.rs -o helloChange its value (Linux / macOS):
MODE=b dcc exec -i hello.rs -o hello -e MODE -- rustc hello.rs -o helloRun with a declared variable (Windows):
$env:MODE="a"; dcc exec -i hello.rs -o hello.exe -e MODE -- rustc hello.rs -o hello.exeChange its value (Windows):
$env:MODE="b"; dcc exec -i hello.rs -o hello.exe -e MODE -- rustc hello.rs -o hello.exeExpect a miss on the second run, because MODE is declared and its value changed.
Change a variable you did not declare (Linux / macOS):
OTHER=1 dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloExpect a hit. OTHER isn't in the key, so changing it changes nothing. This is the boundary described above, made visible.
Check the outcome of any run with dcc stats. The exact log wording depends on the version.
| Choice | Why |
|---|---|
| Content hashes, not timestamps | Timestamps change on checkouts and fresh clones without the content changing, and can stay the same when content does. Hashing bytes answers the real question. |
| Hash each vector, then combine | Fixed-width digests can't be confused with each other, so a value moving from one vector to another can't produce the same key. |
| Declared environment only | Hashing the whole environment would make the key change with every shell session. Declaring the relevant variables keeps keys stable and puts you in control. |
| Hash the tool binary | A compiler upgrade can change output with identical inputs. Including the tool makes stale results from an old version impossible. |
| Include the platform | The same source can compile to different bytes on different OSes or architectures, so results are never replayed across them. |
┌──────────────────────────────────────────────────────────────────────────────┐
│ WITHOUT SINGLE-FLIGHT three runners, same task │
│ │
│ A ████████████████████ │
│ B ████████████████████ │
│ C ████████████████████ total work: 3 × the command's runtime │
│ │
├──────────────────────────────────────────────────────────────────────────────┤
│ WITH SINGLE-FLIGHT same three runners │
│ │
│ A ████████████████████ │
│ B ░░░░░░░░░░░░░░░░░░░░▒ total work: 1 × runtime + 2 small replays │
│ C ░░░░░░░░░░░░░░░░░░░░▒ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
░ waiting on the lock ▒ verify + replay Illustrative, not to scale.
On a CI farm or a parallel build, several workers often ask for the same computation at the same moment. Without coordination, they all miss the cache together and all do the same work (the "thundering herd"). Reforge prevents this with single-flight execution: for each cache key, exactly one runner executes the command, and everyone else waits and reuses its result.
sequenceDiagram
autonumber
participant A as Runner A
participant B as Runner B
participant C as Runner C
participant L as Lock (this key)
participant S as Store
A->>S: look up key
B->>S: look up key
C->>S: look up key
S-->>A: miss
S-->>B: miss
S-->>C: miss
A->>L: acquire
L-->>A: granted
B->>L: acquire
C->>L: acquire
Note over B,C: blocked, waiting for A
A->>A: execute the command
A->>S: commit the entry
A->>L: release
L-->>B: granted
B->>S: check again
S-->>B: hit
L-->>C: granted
C->>S: check again
S-->>C: hit
B->>B: replay outputs
C->>C: replay outputs
Waiters take the lock one at a time. Each one re-checks the store first, finds A's entry, and never runs the command.
| Mechanism | How it works | What it prevents |
|---|---|---|
| 🔐 Per-key advisory lock | Each cache key has its own lock file in .dcc/locks/. The OS grants it to one runner at a time and makes the others wait. |
Duplicate execution of the same task across processes. |
| 🔁 Double-checked lookup | After acquiring the lock, a runner looks in the store again before deciding to execute. | A waiter re-running work that the previous lock holder just finished. |
| 🧯 Crash-safe release | The OS releases the lock when its holder's process ends, including after kill -9. |
Stale locks and deadlocks after a worker dies mid-run. |
flowchart TB
A([dcc exec]) --> B{Entry in store<br/>and verified?}
B -- "yes" --> R1[/✅ Replay outputs/]
B -- "no" --> L["Acquire per-key lock<br/>(waits if another runner holds it)"]
L --> C{Entry appeared<br/>while waiting?}
C -- "yes" --> U1[Release lock] --> R2[/✅ Replay outputs<br/>command never runs/]
C -- "no" --> X["Execute the command"]
X --> S["Commit entry to the store"]
S --> U2[Release lock] --> R3[/📦 Restore outputs/]
The first lookup makes ordinary hits fast, since they never touch the lock. The second one, after the lock is granted, is what makes the race safe.
Between "I saw a miss" and "I got the lock", another runner may have finished the job.
| ❌ Without the re-check | ✅ With the re-check |
|---|---|
|
B sees a miss and waits for the lock. A finishes and releases it. B gets the lock and runs the command again, even though A's result is sitting in the store. |
B sees a miss and waits for the lock. A finishes and releases it. B gets the lock, looks again, finds A's entry, and replays it. |
| Situation | What happens |
|---|---|
The lock holder is killed (kill -9, crash, power loss) |
The OS releases the lock. The next waiter gets it, re-checks, finds nothing stored, and executes the command itself. No manual cleanup. |
| A lock file is left on disk | Harmless. The file is only a name for the OS lock, and it isn't "held" unless a live process holds it. |
| Runners want different keys | They use different locks, so they run in parallel and never block each other. |
| A stored object fails its checksum | The entry is quarantined and treated as a miss, so the lock holder re-executes. |
Note
Advisory locks coordinate cooperating processes. They stop dcc runners from stepping on each other. They don't stop an unrelated program from touching the same files. Lock behaviour also depends on the filesystem, and locking on network filesystems (NFS, SMB) can be unreliable, so test before sharing one .dcc/ directory across machines. Coordination is local to one cache directory. It doesn't span separate caches or separate hosts.
| ✅ Covered | |
|---|---|
Many dcc processes on one machine sharing a cache directoryCI jobs running in parallel against the same workspace cache Crash of any runner at any point |
Runners on different machines with separate caches Non- dcc programs writing into the same filesShared network filesystems with unreliable locking |
A slow command makes the effect visible. This one takes about five seconds. Run it from an empty folder.
Create an input file:
echo data > input.txtStart three identical runs at the same time:
for i in 1 2 3; do dcc exec -i input.txt -o out.txt -- sh -c "sleep 5 && echo done > out.txt" & done; waitCheck the counts:
dcc statsThe three runs should finish in about five seconds total, not fifteen. dcc stats should show one miss and two hits.
Run this in three PowerShell windows within a second or two of each other:
dcc exec -i input.txt -o out.txt -- powershell -Command "Start-Sleep 5; 'done' | Out-File out.txt"Check the counts:
dcc statsReset between attempts (Linux / macOS):
rm -rf .dcc out.txtReset between attempts (Windows):
Remove-Item -Recurse -Force .dcc, out.txtThe repository includes a test that races many processes on the same key.
Run the concurrency suite:
cargo test -p dcc-runner --test concurrency_tests ┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ DISK BUDGET ██████████████████████████████████████░░░░░░░ over limit │
│ │
│ │ 1. EVICT pick entries by strategy (LRU · LFU · FIFO) │
│ ▼ │
│ DISK BUDGET ████████████████████████░░░░░░░░░░░░░░░░░░░░░░ entries gone │
│ │
│ │ 2. GC delete objects no entry refers to anymore │
│ ▼ │
│ DISK BUDGET ██████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░ under limit │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Illustrative, not to scale. █ used ░ free
A cache that only grows will eventually fill your disk. Reforge keeps it within a size limit you set, in two stages. Eviction chooses which entries to drop. Garbage collection then deletes the objects that no remaining entry uses. Splitting the work this way is what makes it safe when entries share objects (see CAS de-duplication).
| Strategy | Evicts first | Best when | Watch out for |
|---|---|---|---|
| 🕐 LRU Least Recently Used |
The entry with the oldest last-access time. | Work comes in phases: what you used lately is what you'll use next. A good default for developer machines. | A long-idle entry that is normally popular can still be evicted. |
| 📊 LFU Least Frequently Used |
The entry with the fewest total accesses. | A stable set of tasks is reused constantly, such as shared CI caches. | New entries start with few accesses, so they can be evicted before they have had a chance to prove useful. |
| 📥 FIFO First In, First Out |
The entry with the oldest creation time. | You want simple, predictable turnover and don't care about usage. | It ignores use entirely, so a very popular entry can be evicted just because it's old. |
Five entries, and the cache is over its limit. Each strategy ranks them differently.
| Entry | Created | Last used | Times used | Note |
|---|---|---|---|---|
| A | day 1 | day 9 | 2 | oldest entry |
| B | day 2 | day 3 | 40 | popular, but idle |
| C | day 3 | day 8 | 5 | |
| D | day 4 | day 4 | 1 | used once |
| E | day 5 | day 10 | 12 | recent and used |
| Strategy | Eviction order (first to go on the left) | First victim |
|---|---|---|
| 🕐 LRU | B → D → C → A → E |
B: nothing has touched it since day 3, despite its 40 uses. |
| 📊 LFU | D → A → C → E → B |
D: it has been used only once. |
| 📥 FIFO | A → B → C → D → E |
A: it was created first, though it was used recently. |
Example data is made up to show the difference. Entries are removed in order until the cache is back under its limit.
flowchart TB
A([dcc prune]) --> B{Cache over<br/>the size limit?}
B -- "no" --> Z[/Nothing to evict/]
B -- "yes" --> C["Rank entries by the chosen strategy<br/>LRU · LFU · FIFO"]
C --> D["Remove entries one by one,<br/>starting with the first victim"]
D --> E{Under the limit<br/>yet?}
E -- "no" --> D
E -- "yes" --> F["Garbage-collect objects<br/>that no entry refers to"]
F --> G[/✅ Cache within budget/]
Removing an entry doesn't automatically free its objects. Another entry may still need them.
flowchart LR
subgraph BEFORE["Before eviction"]
EA["📄 Entry A"] --> O1[("🧱 3a89f4b…")]
EB["📄 Entry B"] --> O1
EB --> O2[("🧱 e712c90…")]
end
subgraph AFTER["After evicting Entry B, then GC"]
EA2["📄 Entry A"] --> O1b[("🧱 3a89f4b…<br/>kept: A still uses it")]
O2b[("🧱 e712c90…<br/>deleted: nothing refers to it")]
end
BEFORE --> AFTER
| Object state | What GC does |
|---|---|
| Referenced by at least one entry | Keeps it, even if other entries that shared it were evicted. |
| Referenced by no entry | Deletes it and frees the disk space. |
Before storing a new object, Reforge checks it against the capacity limit. An object larger than the whole limit is rejected up front, and nothing else is disturbed.
| ❌ Without the check | ✅ With the check |
|---|---|
|
An oversized object arrives. Eviction removes entry after entry trying to make room. The object still doesn't fit, and the useful cache is gone for nothing. |
An oversized object arrives. It's rejected immediately. The existing cache is untouched. |
| Your situation | Try |
|---|---|
| Personal machine, work moves between projects and branches | lru |
| CI cache, the same tasks recur constantly | lfu |
| You want the simplest, most predictable turnover | fifo |
| Not sure | lru, then check dcc stats and compare hit rates |
See how full the cache is:
dcc statsPreview a prune without deleting anything (always do this first):
dcc prune --strategy lru --max-size-mb 500 --dry-runPrune with LRU:
dcc prune --strategy lru --max-size-mb 500Prune with LFU:
dcc prune --strategy lfu --max-size-mb 500Prune with FIFO:
dcc prune --strategy fifo --max-size-mb 500Check the result:
dcc statsTip
Run the --dry-run command with each strategy and compare what each would remove. It's a safe way to see the differences from the table above on your own data.
Note
Eviction removes cached results, never your source files or real outputs. An evicted entry just turns into a miss the next time it's needed, and the command runs again. Eviction costs time, not correctness. A smaller limit means more misses, so size it to your working set.
| ✅ Guaranteed by the design | |
|---|---|
|
Objects shared by surviving entries are never deleted Oversized objects are rejected before insertion Strategies only change which entries go, not correctness --dry-run shows the plan before anything is removed
|
An evicted entry costs a full re-run the next time LFU can penalize brand-new entries FIFO ignores how useful an entry is A limit smaller than your working set causes constant churn |
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ 📖 READ 🔍 CHECK 🚧 ISOLATE ♻️ RECOVER │
│ │
│ load object ──► SHA-256 matches ──► ✅ use it │
│ │ the filename? │
│ │ │
│ └────────► ❌ mismatch ──────► move to quarantine ──► re-run command │
│ └─► store fresh │
│ entry │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
A bad entry costs one extra run. It never reaches your workspace.
Disks fail, power drops mid-write, and files get edited by accident. A cache that trusts what it reads would hand you corrupted output and call it a hit. Reforge doesn't trust the disk: every object is checked against its own hash before it's used. When a check fails, the entry is set aside and the command runs again, so the failure costs you time and not correctness.
sequenceDiagram
autonumber
participant R as Runner
participant S as Store
participant Q as Quarantine
participant C as Your command
participant W as Workspace
R->>S: look up key
S-->>R: entry found
R->>S: read object, re-hash bytes
S-->>R: hash does not match the filename ❌
R->>Q: move the bad entry aside
Note over R,Q: kept for inspection, never replayed
R->>R: log a warning
R->>C: execute fresh
C-->>R: stdout, stderr, exit code, outputs
R->>S: store a new, valid entry
R->>W: restore outputs (atomic)
| Record | How it's verified | If it fails |
|---|---|---|
🧱 Objectscas/objects/ |
The bytes are re-hashed with SHA-256 and compared to the object's filename, which is its expected hash. No extra bookkeeping is needed. | The entry that uses it is quarantined and the command re-runs. |
📄 Entriescas/entries/ |
Metadata linking a cache key to its object digests is validated when read. Unreadable or inconsistent metadata is handled like a bad object. | Quarantined, then recomputed. |
| What went wrong | How it shows up | Result |
|---|---|---|
| Hardware fault or bit rot a flipped bit on disk |
The re-computed hash differs from the filename. | Quarantined, re-run. |
| Power loss or crash mid-write | Bytes are cut short. Writes go through a temp file and an atomic rename, so a partial object normally never gets a real name. If one appears anyway, the hash catches it. | Quarantined, re-run. |
| Accidental hand-edit someone changed a file in .dcc/ |
The content no longer matches its name. | Quarantined, re-run. |
| Tampering with stored bytes | Changed bytes give a different hash. | Quarantined, re-run. |
| Data that fails its check is never written into your workspace. You either get verified bytes or a fresh run. |
Bad entries are moved to .dcc/cas/quarantine/, not deleted. You can look at what went wrong, and the build isn't interrupted.
|
A warning is logged, the command re-runs, and a fresh valid entry replaces the bad one. Nobody has to intervene. |
List what's in quarantine:
dcc quarantine listScan the whole cache for corrupted objects, without waiting for a read to hit them:
dcc verifyScan and recover bad entries:
dcc verify --repairEmpty the quarantine once you're done inspecting it:
dcc quarantine clearNote
dcc quarantine clear deletes the quarantined files for good. If you want to investigate why data went bad, look first and clear afterwards.
| Check | dcc verify (proactive) |
Verify on read (automatic) |
|---|---|---|
| When it runs | When you run it | Every time an object is used |
| What it covers | The whole cache | Only the objects this run needs |
| Good for | After a crash, a disk scare or a restored backup | Everyday safety, with no action from you |
Use the hello.rs demo from the Quick Start. You'll damage a stored object and watch Reforge notice.
Fill the cache with a first run:
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloDelete the output, so a replay has to restore it (Linux / macOS):
rm helloDelete the output, so a replay has to restore it (Windows):
del hello.exeCorrupt one stored object by appending a byte (Linux / macOS):
echo corrupt >> "$(find .dcc/cas/objects -type f | head -n 1)"Corrupt one stored object by appending a byte (Windows):
Add-Content (Get-ChildItem .dcc\cas\objects -Recurse -File | Select-Object -First 1).FullName "corrupt"Run the same command again:
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloExpect a warning, then a normal run. The output file is rebuilt from a fresh execution, not from the damaged object.
See the quarantined entry:
dcc quarantine listConfirm the cache is healthy again:
dcc verifyOn Windows, use hello.exe in place of hello in the dcc exec and rustc commands. The exact warning text depends on the version.
Note
This detects changes. It doesn't stop them. The checksum tells Reforge that stored data no longer
┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ 📥 a path arrives 🚧 THREE CHECKS 📁 result │
│ (a flag, or a cache │
│ entry being restored) │
│ │
│ ../../etc/passwd ──► 1. absolute / ../ sanitized ──► ❌ rejected │
│ link/out.txt ──► 2. symlinks resolved ──► ❌ rejected │
│ target/gen/a.rs ──► 3. canonical path inside ──► ✅ written │
│ workspace_dir? │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Only paths that end up inside the project workspace are ever written.
When Reforge restores a cached result, it writes files into your project. That is the moment a bad path could do damage: an output declared as ../../somewhere/else, a folder that is secretly a symlink to a system directory, or a damaged or hostile cache entry that lists an absolute path. Reforge checks every output path before writing, and refuses anything that would land outside the workspace.
| Protection | The attack it stops | How it works | Outcome |
|---|---|---|---|
| 📍 Canonical path containment | An output path that reaches outside the project through .. or a roundabout route. |
Every declared output path is canonicalized (resolved to its one true absolute form) and must lie strictly inside workspace_dir. |
Rejected |
| 🔗 Symlink escape prevention | Writing through a symlinked directory that points at /etc, C:\Windows or any other outside location. |
Outputs that pass through an intermediate directory symlink to an external location are refused. | Rejected |
| 🧼 Absolute path sanitization | A malicious or damaged cache entry that lists /etc/cron.d/x or ../../.ssh/authorized_keys as an output. |
Absolute paths and ../ traversal tokens can't escape the designated root, even when they come from stored metadata and not from your command line. |
Rejected |
A path that looks safe can point somewhere else once resolved.
| Path as written | Where it really goes |
|---|---|
gen/../../secrets.txt |
One folder above the project. It starts in gen/ but climbs out. |
build/out.txt, where build is a symlink to /etc |
/etc/out.txt, outside the workspace, although the text looks harmless. |
/home/you/.bashrc |
An absolute path with no relation to the project. |
Comparing the text of a path isn't enough. Reforge resolves the real destination first and judges that.
flowchart TB
A(["Output path<br/>from your flags or a cache entry"]) --> B{"Absolute path or<br/>../ traversal?"}
B -- "yes" --> X[/"❌ Rejected"/]
B -- "no" --> C["Resolve to the canonical path,<br/>following symlinks"]
C --> D{"Inside<br/>workspace_dir?"}
D -- "no" --> X
D -- "yes" --> E{"Passes through a directory<br/>symlink to an outside location?"}
E -- "yes" --> X
E -- "no" --> F["Stage outputs in a temp location"]
F --> G[/"✅ Rename into place"/]
| Who or what | What Reforge does |
|---|---|
| A mistaken flag you typed -o ../out |
Rejects the path before anything is written outside the project. |
| A damaged cache entry | Verifies it, and rejects paths that would escape. See Self-Healing & Quarantine. |
| A hostile cache entry a crafted entry that lists an escaping path |
The path checks apply to restored entries too, so replaying one can't write outside the workspace. |
| A symlink planted in the workspace | Writes that would travel through it to an outside location are refused. |
Important
Reforge confines where it writes restored files. It does not sandbox the command you run. The wrapped command executes with your normal user permissions and can read or write anything you can. Only run commands you trust, exactly as you would without dcc. Path confinement protects the restore step. It isn't process isolation.
| ✅ Covered | |
|---|---|
Output paths that climb out with ../Absolute output paths Directory symlinks leading outside the workspace Hostile paths inside stored cache entries |
What the command itself reads, writes or runs Files the command writes that you didn't declare with -oNetwork access by the command Other users who can write to your .dcc/ directory
|
| Do | Avoid |
|---|---|
|
Keep declared outputs inside the project folder. Keep .dcc/ owned by you, with normal file permissions.Review commands before wrapping them in dcc exec.Run dcc verify after restoring a cache from backup or another source.
|
Sharing a writable .dcc/ with users or processes you don't trust.Copying a cache from an untrusted source and running with it as-is. Assuming dcc limits what the wrapped command can touch.
|
Run these in an empty folder. Every command that should be refused is a safe test, because it uses harmless commands and creates nothing.
Create an input file:
echo data > input.txtDeclare an output outside the workspace:
dcc exec -i input.txt -o ../escaped.txt -- echo testExpect a rejection with an error about the path leaving the workspace. Nothing should be created outside the folder.
Confirm nothing was written (Linux / macOS):
ls ../escaped.txtConfirm nothing was written (Windows):
dir ..\escaped.txtBoth should report that the file doesn't exist.
Declare an absolute output path (Linux / macOS):
dcc exec -i input.txt -o /tmp/dcc-escape-test.txt -- echo testDeclare an absolute output path (Windows):
dcc exec -i input.txt -o C:\Windows\Temp\dcc-escape-test.txt -- echo testExpect a rejection.
Create a folder outside the workspace and a symlink to it (Linux / macOS):
mkdir ../outside && ln -s ../outside linkDeclare an output through the symlink:
dcc exec -i input.txt -o link/out.txt -- echo testCheck the outside folder stayed empty:
ls ../outsideExpect a rejection, and an empty folder.
Windows: creating a symlink needs Developer Mode or an elevated prompt.
mkdir ..\outside; cmd /c mklink /D link ..\outsideThen run the same dcc exec command as above, and check with dir ..\outside.
The exact error wording depends on the version. What matters is that the run is refused and nothing appears outside the workspace.
Run the security tests:
cargo test -p dcc-runner --test security_tests ┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ dcc │
│ ├── exec run a command with caching │
│ ├── stats show usage, hit/miss counts, capacity │
│ ├── verify check every stored object against its hash │
│ ├── prune enforce a size limit (LRU · LFU · FIFO) │
│ └── quarantine │
│ ├── list show entries that failed verification │
│ └── clear delete them for good │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
| Command | What it does | Changes data? | Jump to |
|---|---|---|---|
dcc exec |
Runs a command, or replays its stored result. | Yes (adds entries, writes outputs) | ↓ |
dcc stats |
Shows cache usage, hit and miss counts, and capacity. | No | ↓ |
dcc verify |
Scans the cache for corrupted objects. | Only with --repair |
↓ |
dcc prune |
Removes entries until the cache fits a size limit. | Yes (use --dry-run first) |
↓ |
dcc quarantine |
Lists or clears entries that failed verification. | list no · clear yes |
↓ |
Syntax:
dcc exec [OPTIONS] -- <COMMAND> [ARGS]...
dcc exec -i src/ -o gen/ -e MODE --dir app -- protoc --rust_out=gen a.proto
│ │ │ │ │ └── everything after -- is your command
│ │ │ │ └────── the separator: dcc's flags end here
│ │ │ └──────────────────── where to run the command
│ │ └─────────────────────────────── env variables that affect the result
│ └───────────────────────────────────────── files the command writes
└─────────────────────────────────────────────────── files the command reads
Important
Always put -- before your command. It tells dcc where its own options end. Without it, flags meant for your command (like -o for rustc) could be read as dcc options.
| Option | Meaning | Part of the cache key? |
|---|---|---|
-i, --input <PATH> |
A file or directory the command reads. Repeat the flag for more than one. | Yes. Contents are hashed. |
-o, --output <PATH> |
A file or directory the command writes. Captured after a run, restored on a hit. | No. These are what gets stored. |
-e, --env <VAR> |
An environment variable whose value affects the result. | Yes. Name and value are hashed. |
--dir <PATH> |
The working directory to run the command in. Defaults to the current directory. | See the checklist below. |
--no-stdout |
Don't capture or replay standard output. | See the checklist below. |
--no-stderr |
Don't capture or replay standard error. | See the checklist below. |
Besides declared outputs, dcc also stores and replays the command's stdout, stderr and exit code, unless you turn those off. See how the key is built.
Minimal: one input, one output:
dcc exec -i hello.rs -o hello -- rustc hello.rs -o helloSeveral inputs (repeat -i):
dcc exec -i main.cpp -i include/ -o main.o -- clang++ -O3 -c main.cpp -o main.oSeveral outputs (repeat -o):
dcc exec -i proto/ -o gen/ -o docs/api.md -- ./generate.shA directory as an input (everything inside it is hashed):
dcc exec -i src/ -o dist/ -- npx esbuild src/index.ts --bundle --outdir=distDeclare an environment variable that affects the result:
dcc exec -i src/ -o dist/ -e NODE_ENV -- npx esbuild src/index.ts --bundle --outdir=distDeclare several variables (repeat -e):
dcc exec -i src/ -o dist/ -e NODE_ENV -e API_URL -- ./build.shRun in a different directory:
dcc exec --dir app -i src/ -o dist/ -- npm run buildDon't replay standard output:
dcc exec --no-stdout -i input.txt -o out.txt -- ./process.shDon't replay standard error:
dcc exec --no-stderr -i input.txt -o out.txt -- ./process.shTurn off both:
dcc exec --no-stdout --no-stderr -i input.txt -o out.txt -- ./process.shWarning
A hit is only as safe as your declarations. Every file the command reads belongs under -i, every variable that changes its result belongs under -e, and every file it writes belongs under -o. See the Core Guarantee.
Syntax:
dcc stats [--json]
Human-readable summary:
dcc statsMachine-readable output for scripts:
dcc stats --jsonPretty-print the JSON with jq, if you have it installed:
dcc stats --json | jq .Use it to check hit and miss counts, see how full the cache is against its limit, and confirm that a run behaved the way you expected.
Syntax:
dcc verify [--repair]
Scan the whole cache for corrupted objects:
dcc verifyScan and recover bad entries:
dcc verify --repairObjects are re-hashed and compared with their names, as described in Self-Healing & Quarantine. Run it after a crash, a disk scare or restoring a cache from a backup. Ordinary runs already verify what they read.
Syntax:
dcc prune --strategy <lru|lfu|fifo> --max-size-mb <MB> [--dry-run]
| Option | Meaning |
|---|---|
--strategy | Which entries go first: lru (least recently used), lfu (least frequently used) or fifo (oldest created). See Eviction & GC. |
--max-size-mb | The size limit, in megabytes. |
--dry-run | Show what would be removed, and remove nothing. |
Preview first, always:
dcc prune --strategy lru --max-size-mb 500 --dry-runPrune to 500 MB, least recently used first:
dcc prune --strategy lru --max-size-mb 500Prune to 500 MB, least frequently used first:
dcc prune --strategy lfu --max-size-mb 500Prune to 500 MB, oldest first:
dcc prune --strategy fifo --max-size-mb 500Pruning only removes cached results. A removed entry becomes a miss the next time it's needed, and the command runs again.
Syntax:
dcc quarantine <list|clear>
See what failed verification:
dcc quarantine listDelete the quarantined files permanently:
dcc quarantine clearNote
Look with list before you clear. Clearing deletes the evidence of what went wrong.
| When you want to… | Do this |
|---|---|
| Cache a slow command | Wrap it in dcc exec with -i, -o and -e declared. |
| Check that caching works | Run the same command twice, then dcc stats. Expect one miss and one hit. |
| Keep the cache from growing | dcc prune --dry-run first, then the same command without the flag. |
| Recover after a crash or a disk fault | dcc verify, then dcc verify --repair if it reports problems. |
| Investigate corruption | dcc quarantine list before clearing anything. |
| Feed numbers into a script or dashboard | dcc stats --json. |
| Mistake | What happens and how to fix it |
|---|---|
Leaving out -- |
Flags for your command can be confused with dcc options. Always separate them: dcc exec … -- your-command. |
| Forgetting an input | Editing that file doesn't invalidate the cache, and you get a stale hit. Declare everything the command reads. |
| Forgetting an output | The file isn't captured, so it's missing after a hit. Declare everything the command writes. |
| Declaring a generated file as an input | It changes every run, so you always get a miss. |
Pruning without --dry-run |
Entries are removed with no preview. Preview first. |
| Clearing quarantine too early | The evidence is deleted. Run list first. |
| Command | Purpose |
|---|---|
dcc exec [OPTIONS] -- <COMMAND> [ARGS]... | Run with caching |
dcc stats [--json] | Usage and hit/miss metrics |
dcc verify [--repair] | Integrity scan, optionally repair |
dcc prune --strategy <lru|lfu|fifo> --max-size-mb <MB> [--dry-run] | Enforce capacity |
dcc quarantine list | Show quarantined entries |
dcc quarantine clear | Delete quarantined entries |
dcc is language-agnostic by design.
It does not replace your compiler, bundler, generator, test runner, or static analyzer. Instead, it sits around the existing command and adds deterministic computation caching to workflows that were never designed with caching in mind.
Whether the workload is Rust compilation, a C++ translation unit, a TypeScript bundle, or a Python analysis pass, the execution model remains the same:
┌──────────────────────────────┐
│ Developer Task │
│ Build · Generate · Test · CI│
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ dcc exec │
│ │
│ Hash → Resolve → Lock → │
│ Execute / Replay → Restore │
└──────────────┬───────────────┘
│
┌─────────────┴─────────────┐
│ │
CACHE HIT CACHE MISS
│ │
▼ ▼
CAS Replay Run Original Tool
+ Output Restore + Capture Outputs
│ │
└─────────────┬─────────────┘
▼
Deterministic Result
| Ecosystem | Typical Workload | What dcc Tracks |
Example Tool |
|---|---|---|---|
| 🦀 Rust | Compilation, code generation, tests | Source tree, arguments, environment, toolchain | cargo |
| ⚙️ C / C++ | Translation units, generated artifacts | Source/header inputs, compiler flags, tool identity | clang++ |
| 🌐 Web / TypeScript | Bundling and asset generation | Source tree, package metadata, build arguments | esbuild |
| 🐍 Python | Static analysis, linting, generated artifacts | Package sources, configuration, environment | mypy |
The integration boundary stays intentionally thin:
dccobserves the declared inputs and outputs, while the underlying tool remains responsible for the actual computation.
Cache expensive compilation, code-generation, or test workflows without changing the underlying Cargo command.
dcc exec \
-i src/ \
-o target/out.bin \
-- cargo build --releaseExecution flow:
src/
│
├── Rust source files
├── Cargo configuration
└── declared build inputs
│
▼
dcc CacheKey
│
┌────┴────┐
│ │
MISS HIT
│ │
▼ ▼
cargo CAS Replay
build + Restore
Cache standalone compiler invocations while incorporating both source files and header dependencies into the declared input set.
dcc exec \
-i main.cpp \
-i include/ \
-o main.o \
-- clang++ -O3 -c main.cpp -o main.oChanging a tracked source/header or compiler argument produces a different cache key, preventing the previous result from being reused.
Cache deterministic frontend build and bundling operations across JavaScript/TypeScript projects.
dcc exec \
-i src/ \
-i package.json \
-o dist/ \
-- npx esbuild src/index.ts \
--bundle \
--outdir=distThis allows unchanged source trees and build parameters to replay previously generated artifacts directly from the local CAS.
Cache static-analysis workflows where repeatedly scanning an unchanged package would otherwise perform the same computation.
dcc exec \
-i my_package/ \
-o .mypy_cache/ \
-- mypy my_package/The same execution model applies to compatible linters, analyzers, generators, and other deterministic command-line workloads.
Regardless of the ecosystem, integration follows one consistent pattern:
dcc exec
│
├── -i Input files / directories
├── -o Expected output files / directories
├── -e Environment variables
│
└── -- Original command + arguments
That means teams do not need a separate caching implementation for every language.
Rust C/C++ TypeScript Python
│ │ │ │
└───────────┴─────────────┴──────────────┘
│
▼
┌─────────┐
│ dcc │
└────┬────┘
│
┌─────────────┴─────────────┐
▼ ▼
Cache Miss Cache Hit
Execute Normally Replay from CAS
dcc is intentionally positioned below the language layer and above the operating-system process boundary.
Your build system, compiler, bundler, generator, or analyzer continues doing what it already does. dcc adds the missing execution layer around it:
- No language-specific cache format required
- No rewrite of the underlying tool
- One CLI model across multiple ecosystems
- Content-addressed result storage
- Deterministic invalidation
- Cross-process duplicate-execution prevention
- Atomic output restoration
Bring the command.
dccdecides whether it needs to run.
Reforge is not considered complete simply because its features are implemented.
Each milestone represents a specific engineering capability that has been implemented, exercised, and verified through dedicated automated test suites — covering correctness, concurrency, storage integrity, security, failure recovery, reliability, and performance.
The result is a verification trail spanning the complete execution lifecycle:
┌─────────────────────────────────────────────────────────────────────────────┐
│ REFORGE VERIFICATION PIPELINE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ FOUNDATION DETERMINISM CONCURRENCY STORAGE │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ M1 ─ M4 M5.1 ─ M5.5 M6.1 ─ M6.5 M7 ─ M10 │
│ │ │ │ │ │
│ └─────────────────┴─────────────────┴────────────────┘ │
│ │ │
│ ▼ │
│ INTEGRATIONS │
│ │ │
│ M11 ─ M13 │
│ │ │
│ ┌───────────────┴────────────────┐ │
│ ▼ ▼ │
│ SECURITY RESILIENCE │
│ M14.1 ─ M14.3 M15 ─ M17 │
│ │ │ │
│ └───────────────┬────────────────┘ │
│ ▼ │
│ END-TO-END VERIFICATION │
│ M18 ─ M19 │
│ │ │
│ ▼ │
│ RELEASE ASSURANCE │
│ M20.1 ─ M20.3 │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ VERIFIED │ │
│ │ ENGINE │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
| Milestone | Engineering Capability | Verification Focus | Test Suite | Status |
|---|---|---|---|---|
| M1–M4 | 🧱 Core Foundation | Domain models, SHA-256 digest ingestion, cache-key construction and invalidation | unit_tests_milestone_18_2 |
PASSED ✅ |
| M5.1–M5.5 | 🔐 Deterministic Invalidation | Input files, command arguments, environment variables, tool versions and platform state | integration_tests |
PASSED ✅ |
| M6.1–M6.5 | ⚡ Single-Flight Concurrency | Lock acquisition, contention handling and 50-process concurrent execution | concurrency_tests |
PASSED ✅ |
| M7–M10 | 📦 CAS & Storage Lifecycle | Content-addressable storage, eviction strategies, capacity enforcement and garbage collection | eviction_tests |
PASSED ✅ |
| M11–M13 | 🔌 Polyglot Integrations | Build-tool integrations and benchmark-based speedup analysis | cache-integrations |
PASSED ✅ |
| M14.1–M14.3 | 🛡️ Security Hardening | Directory traversal rejection, symlink escape prevention and filesystem boundary enforcement | security_tests |
PASSED ✅ |
| M15–M17 | 🧯 Failure Recovery | Failure injection, chaos recovery and corrupted-entry quarantine | failure_injection_tests |
PASSED ✅ |
| M18–M19 | 🔄 End-to-End Reliability | Complete workflow lifecycles and high-load same-key writers | concurrency_suite_18_4 |
PASSED ✅ |
| M20.1 | 🧬 Release Integrity | Release-quality validation, metadata self-healing and corrupted CAS recovery | release_audit_milestone_20 |
PASSED ✅ |
| M20.2 | 💪 Reliability Assurance | Process-crash resilience and strict capacity-bound verification | reliability_audit_20_2 |
PASSED ✅ |
| M20.3 | 🚀 Performance Assurance | Execution overhead measurement and cache speedup verification | performance_audit_milestone_20_3 |
PASSED ✅ |
The milestone matrix intentionally progresses from internal correctness to real-world operational behavior:
┌───────────────────────┐
│ CORE CORRECTNESS │
│ Models + Hashing │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ DETERMINISTIC CACHE │
│ Key + Invalidation │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ CONCURRENT EXECUTION │
│ Locks + Single Flight │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ STORAGE INTEGRITY │
│ CAS + GC + Eviction │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ SECURITY & RECOVERY │
│ Isolation + Healing │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ RELEASE ASSURANCE │
│ Reliability + Perf. │
└───────────────────────┘
A milestone is considered verified when its associated implementation and automated verification suite have completed successfully.
The audit therefore evaluates more than the happy path. It explicitly exercises:
- Deterministic cache-key generation
- Multi-dimensional invalidation
- Concurrent same-key execution
- CAS integrity and object lifecycle
- Capacity and eviction behavior
- Filesystem boundary enforcement
- Symlink and traversal rejection
- Corruption detection and quarantine
- Process-failure recovery
- End-to-end execution lifecycles
- Release integrity
- Performance and cache-speedup behavior
The goal is not simply to prove that Reforge works.
The goal is to continuously verify the conditions under which Reforge is allowed to trust a cached computation.
╔══════════════════════════════════════════════════════════╗
║ REFORGE AUDIT STATUS ║
╠══════════════════════════════════════════════════════════╣
║ Engineering Milestones 20+ ║
║ Verification Areas Correctness → Performance ║
║ Automated Suites Dedicated per capability ║
║ Reported Milestone Status PASSED ║
╚══════════════════════════════════════════════════════════╝
The milestone matrix represents the project's current engineering verification record, rather than a claim that every possible workload or environment has been exhaustively tested.
┌──────────────────────────────────────────────────────────────────────────────────┐
│ │
│ Every guarantee in this README has a test suite behind it. │
│ │
│ 🔒 key invalidation 🏎️ concurrency 🛡️ self-healing 🔐 security │
│ ⚙️ eviction 🔌 integrations 📋 release, reliability,performance │
│ │
└──────────────────────────────────────────────────────────────────────────────────┘
Reforge's promises (a hit never changes the result, racing runners share one execution, corrupt data is never replayed, outputs can't escape the workspace) are each checked by automated tests. You can run all of them at once or one guarantee at a time.
All unit and integration tests across the workspace:
cargo test --workspace| Suite | What it checks | Guarantee |
|---|---|---|
security_tests |
Output paths can't escape the workspace: ../ traversal, absolute paths and symlink escapes are rejected. |
Security |
concurrency_tests |
Many processes racing on one key: lock contention, one execution, the rest replay. | Single-flight |
failure_injection_tests |
Injected crashes and corrupted bytes: detection, quarantine and recovery. | Self-healing |
release_audit_milestone_20 |
Release-quality audit, including metadata self-healing and corrupted CAS data. | CAS |
reliability_audit_milestone_20_2 |
Process-crash resilience and capacity bounds. | Eviction |
performance_audit_milestone_20_3 |
Overhead and speedup verification. | Performance |
Security and directory-escape prevention:
cargo test -p dcc-runner --test security_testsConcurrency and lock contention:
cargo test -p dcc-runner --test concurrency_testsFailure injection and chaos self-healing:
cargo test -p dcc-runner --test failure_injection_testsRelease audit:
cargo test -p dcc-runner --test release_audit_milestone_20Reliability audit:
cargo test -p dcc-runner --test reliability_audit_milestone_20_2Performance audit (build in release mode so timings are meaningful):
cargo test -p dcc-runner --test performance_audit_milestone_20_3 --releaseThe full milestone breakdown is in the audit matrix.
Core (keys and hashing):
cargo test -p dcc-coreStorage (CAS, eviction, quarantine):
cargo test -p dcc-storageRunner (execution, locking, restore):
cargo test -p dcc-runner| You want to… | Add this to the command |
|---|---|
| See printed output from passing tests | -- --nocapture |
| Run tests one at a time | -- --test-threads=1 |
| Run only tests whose name contains a word | -- <name> |
| List the tests in a suite without running them | -- --list |
| Get a full stack trace on a panic | set RUST_BACKTRACE=1 |
Show output while a suite runs:
cargo test -p dcc-runner --test concurrency_tests -- --nocaptureRun one suite single-threaded (useful if you suspect timing interference):
cargo test -p dcc-runner --test security_tests -- --test-threads=1List the tests in a suite:
cargo test -p dcc-runner --test failure_injection_tests -- --listRun only tests with "symlink" in their name:
cargo test -p dcc-runner --test security_tests -- symlinkGet a full backtrace (Linux / macOS):
RUST_BACKTRACE=1 cargo test -p dcc-runner --test concurrency_testsGet a full backtrace (Windows):
$env:RUST_BACKTRACE=1; cargo test -p dcc-runner --test concurrency_testsNote
Passing tests show that Reforge behaves as specified on the machine that ran them. They don't prove that your own dcc exec commands are declared correctly. A missing -i is invisible to any test suite, so review declarations as described in the Core Guarantee. Timing-based suites, such as concurrency and performance, can vary with machine load. If one fails once, re-run it before assuming a regression.
Reforge is written in Rust and designed to run natively on all three. It uses the operating system's own file locking, atomic rename and path handling, and it doesn't depend on a Unix shell.
| Area | 🪟 Windows | 🍎 macOS / 🐧 Linux | What to do |
|---|---|---|---|
| Output names | Programs end in .exe |
No extension | Declare the real filename with -o. |
| Symlinks | Creating them needs Developer Mode or an elevated prompt | Available to any user | Only matters for the symlink demos. |
| Path separators | \ and / |
/ |
Use forward slashes in scripts you share across systems. |
| Filename case | Usually case-insensitive | Linux is case-sensitive. macOS usually isn't. | Keep casing consistent so the same command hashes the same paths everywhere. |
| Shell in your command | PowerShell or cmd |
sh, bash or zsh | Wrap the tool directly (cargo, rustc) instead of shell one-liners where you can. |
The cache key includes the OS, architecture, pointer width and endianness. A result built on Linux is never replayed on Windows or macOS, or on a different CPU architecture. That's intentional, since the same source can compile to different bytes on each. It does mean each platform builds its own cache.
Note
Locking and atomic renames work as designed on local disks. Network filesystems (NFS, SMB) and some synced folders can behave differently, so keep .dcc/ on a local drive and don't share one writable cache directory over a network without testing it first.
Build:
cargo build --workspaceRun the tests:
cargo test --workspaceRun the concurrency suite, which exercises locking on your OS:
cargo test -p dcc-runner --test concurrency_testsRun the security suite, which exercises path handling on your OS:
cargo test -p dcc-runner --test security_testsFound a difference on your platform? Please open an issue with your OS, architecture and the command you ran.
Reforge is dual-licensed. You may use it under either of these licenses, whichever suits your project:
⚖️ MIT License · 📜 Apache License 2.0
MIT is short and simple. Apache-2.0 adds an explicit patent grant.
| MIT | Apache-2.0 | |
|---|---|---|
| Style | Very short and simple | Longer and more detailed |
| Commercial use, modification, redistribution | Allowed | Allowed |
| Explicit patent grant | No | Yes |
| Keep copyright and license notice | Yes | Yes, and mark files you changed |
| Often chosen when | You want the simplest terms | You want the patent protection |
This table is a summary to help you choose, not legal advice. The license files are what count.
Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in Reforge, as defined in the Apache-2.0 license, will be dual-licensed as above, without any additional terms or conditions.
Contributions of every kind are welcome: bug reports, fixes, tests, documentation and ideas. Please review our Contributing Guide and Code of Conduct for full engineering guidelines and workflows.
| Contribution | What helps most |
|---|---|
| Report a bug | Your OS and architecture, the exact dcc exec command, what you expected and what happened. A stale cache hit is a real bug: say which file wasn't declared. |
| Propose a feature | Describe the problem first, then your idea. For anything touching cache keys, eviction or path checks, open an issue before writing code. |
| Send a fix | One change per pull request, with a test for any behaviour you change. Small pull requests get reviewed fastest. |
| Improve the docs | Try the Quick Start and fix whatever doesn't work. Typos, unclear steps and missing examples all count. |
| Add tests | Failure cases especially: corrupted data, crashes mid-run, unusual paths and platform differences. |
Open an issue here: github.com/Ashish6298/Reforge/issues
Fork the repository on GitHub, then clone your fork:
git clone https://github.com/<your-username>/Reforge.gitEnter the folder:
cd ReforgeCreate a branch:
git checkout -b my-changeBuild everything:
cargo build --workspaceRun the tests (before and after your change):
cargo test --workspaceFormat the code:
cargo fmt --allLint it:
cargo clippy --workspace --all-targetsPush your branch, then open a pull request on GitHub:
git push origin my-change- The change does one thing.
-
cargo test --workspacepasses. - New or changed behaviour has a test.
-
cargo fmtandcargo clippyare clean. - The README is updated if a command or behaviour changed.
- You say which OS you tested on.
- No safety check was weakened (hash verification, path confinement, locking).
- Any change to cache-key rules is explained, since it invalidates users' caches.
| If you change | Keep in mind |
|---|---|
Cache key rules (crates/cache-core) |
A change can silently invalidate every user's cache, or keep a stale one. Discuss it in an issue first. |
Storage and eviction (crates/cache-storage) |
Objects shared by surviving entries must not be deleted, and every read must stay verified. |
Locking, restore and paths (crates/cache-runner) |
Outputs must never land outside the workspace, and a killed runner must never leave the cache stuck. |
Please don't open a public issue for a vulnerability. Contact the maintainer privately through the GitHub profile first, and include what you found, how to reproduce it and which platform you used. Public disclosure can wait until a fix is ready.
Reforge
Built with ❤️ in Rust for high-performance developer workflows.