Haskell · Cabal · build tooling
Cabal Sandboxes: What They Were, and What Replaced Them
cabal sandbox itself is now historical — if you just want the modern workflow, skip to what to use today.When cabal sandbox landed in cabal-install 1.18 in August 2013, it fixed the single most painful thing about Haskell development at the time: the global package database. Before sandboxes, every cabal install wrote into one shared ~/.cabal store. Install two projects that needed different versions of the same library and you got the infamous "dependency hell" — a corrupted global database that often ended with rm -rf ~/.ghc ~/.cabal and starting over.
Sandboxes made dependencies per-project. That principle never went away — but the mechanism did. Here is the full picture, from how sandboxes worked to the nix-style workflow that replaced them.
What a sandbox actually was
A sandbox was a project-local package database. You created one in your project directory:
$ cd my-project
$ cabal sandbox init
Writing a default package environment file to
/home/you/my-project/cabal.sandbox.config
Creating a new sandbox at
/home/you/my-project/.cabal-sandbox From then on, cabal install and cabal build run inside that directory used .cabal-sandbox/ instead of the global store. Two projects could depend on conflicting versions of aeson or text without ever interfering, because each had its own isolated database.
Local source dependencies
The other genuinely useful feature was add-source, which let a sandbox pull in a library straight from a local checkout — invaluable when hacking on a library and an app together:
$ cabal sandbox add-source ../my-library
$ cabal install --dependencies-only
$ cabal build Edit ../my-library, rebuild the app, and cabal would detect the change and recompile the dependency. This was the closest thing Haskell had to a monorepo workflow in 2013.
Why sandboxes were eventually retired
Sandboxes solved isolation but left two problems unsolved:
- No sharing between projects. Every sandbox recompiled the world from scratch. Ten projects depending on
lensmeant ten separate builds oflensand its enormous dependency tree — slow and disk-hungry. - No reproducibility across machines. A sandbox captured where packages lived, not a precise, repeatable plan of which exact versions would be chosen. Two developers running the same commands a week apart could still get different builds.
The fix, borrowed conceptually from Nix, was to make builds content-addressed and globally shared: build any given package-version-plus-flags combination once, store it under a hash in a global immutable store, and let every project that needs that exact combination reuse it. That is "nix-style local builds", and it arrived as new-build in Cabal 2.0.
What to use today (2026)
cabal sandbox was deprecated during the Cabal 2.x series and removed entirely in later releases. The modern, default workflow in current cabal-install needs no init step at all — isolation is automatic:
# In any project directory with a .cabal file:
$ cabal build # nix-style local build (v2 is the default now)
$ cabal run
$ cabal repl
$ cabal test What happens under the hood is exactly the per-project isolation sandboxes promised, plus the sharing they lacked:
- Build artifacts for your project go in a local
dist-newstyle/directory (the spiritual successor to.cabal-sandbox/). - Compiled dependencies go in a global immutable store (
~/.cabal/store, or under~/.local/state/cabalon newer layouts), keyed by a hash of the package, version, dependencies and flags. Buildlensonce; every project reusing the same plan links the same store entry. - A
cabal.projectfile replacescabal.sandbox.configfor configuration, multi-package builds, and pinning.
The modern equivalent of add-source
Local and remote source dependencies now live in cabal.project:
# cabal.project
packages: . ../my-library
-- or pull a dependency straight from git:
source-repository-package
type: git
location: https://github.com/owner/some-lib
tag: 0a1b2c3d This is strictly more powerful than add-source: it handles multiple local packages, exact git pins, and reproducible multi-repo builds.
Reproducible version pinning
For the reproducibility sandboxes never offered, generate a freeze file that locks every transitive version:
$ cabal freeze # writes cabal.project.freeze
# commit it — every machine now resolves the identical plan What about Stack?
Stack approaches the same goals from a different angle: instead of solving versions per project, it builds against curated snapshots (Stackage resolvers like lts-22.x) where a whole set of packages is known to compile together, and shares builds globally per snapshot. Both tools are healthy and widely used in 2026; the practical split is roughly "I want the standard toolchain and fine-grained control" (Cabal) versus "I want a curated, known-good set out of the box" (Stack). Neither uses sandboxes — the concept is fully superseded on both sides.
Migration cheatsheet
cabal sandbox init → (nothing — just `cabal build`)
cabal sandbox add-source ../lib → packages: . ../lib (in cabal.project)
cabal install --dependencies-only → cabal build --only-dependencies
.cabal-sandbox/ → dist-newstyle/ (local) + ~/.cabal/store (shared)
cabal.sandbox.config → cabal.project (+ cabal.project.freeze) The lesson sandboxes taught — never pollute a global mutable state, isolate per project — won so completely that it is now invisible: it is simply how cabal build behaves. If you are reading this because an old link or tutorial pointed you at cabal sandbox init, you can safely forget the command. Just cabal build.