coldwa.st_

Haskell · Cabal · build tooling

Cabal Sandboxes: What They Were, and What Replaced Them

Originally a landmark topic on this domain (2013) · fully rewritten and updated for 2026 · Coldwast Programming Guides

Three isolated Cabal sandboxes, each holding its own package set, beside a terminal running cabal sandbox init
Per-project dependency isolation — the idea sandboxes introduced, now the default.
This is a community-maintained, updated rewrite of a topic this domain was historically known for. It is original content and is not authored by or affiliated with the domain's previous owner. 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:

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:

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.


Related: Cabal 2.0 and nix-style builds · Parallelising cabal-install · all guides