The Sandbox
The sandbox is a one-command, real-scenario tour of the entire forge lifecycle. It builds the actual forge binary, stages a realistic sample project, and drives it through import, scoring, every enforced gate, and the dashboard build — asserting at each step that the gates fire exactly the way they do in production. It is both the fastest way to see forge work end to end and the repository's end-to-end safety net.
Runs from a source checkout
The sandbox lives in the forge source tree (scripts/sandbox/, wired to make targets), not in an installed project. Clone the repository to run it. Installed users get the same behavior through the individual commands documented across this site — the sandbox just wires them into one realistic run.
Two entry points
Both drive the same 9-step scenario against the same fixtures, so they never drift:
| Command | What it is |
|---|---|
make sandbox | Human-drivable. Builds the binary, runs the full lifecycle, and leaves the artifacts under scripts/sandbox/out/work/ for you to open and inspect. |
make sandbox-clean | The same run with a wiped output directory first (run.sh --clean). Use it when a previous run left stale state. |
make e2e | The hermetic, CI-gated form: a tag-gated Go test (go test -tags e2e ./e2e/...) that runs the identical scenario in a temp directory with hard assertions and no persisted artifacts. |
The script runs every step even if an earlier one failed, prints one PASS/FAIL line per assertion, and exits non-zero if any assertion failed — so a single run reports every problem rather than stopping at the first.
The 9-step lifecycle
Each step asserts a specific, observable outcome. The gate steps are the point: they deliberately expect a non-zero exit to prove the gate blocks.
- build — compile the
forgebinary toout/bin/forge. - stage — copy the
acme-shopfixture into a fresh work directory. - import — map the
.specify/project into.forge/and assert the change directories were created. See Migration Importer. - analyze — run the portfolio health rollup and a single-spec score, asserting the quality score is non-trivial.
- secscan — scan the planted secret and assert the guardrails gate BLOCKS (exit non-zero).
- secscan waive + rescan — waive the finding as a human would, and assert the rescan is now clean.
- budget — accumulate spend, then cross the ceiling and assert the spend budget reports
BUDGET EXCEEDED(exit non-zero). - verify breaker — force a verify failure so the circuit-breaker opens (exit non-zero), then
--reset-breakerand assert it clears. - trace + docs — assert the session timeline aggregated the gate events, then build the offline dashboard and assert
index.htmlwas written.
The gates are supposed to fail
Steps 5, 7, and 8 assert a non-zero exit. That is the sandbox proving the enforced gates actually stop the line — a secret is blocked, a budget ceiling is enforced, a failing verify escalates. A sandbox where every step returns 0 would mean the gates are asleep.
The sample project
The fixture is a small but realistic .specify/ project, acme-shop, committed under scripts/sandbox/fixtures/:
acme-shop/
.specify/
memory/constitution.md # a few real principles
specs/001-checkout/{spec,plan,tasks}.md
specs/002-auth/{spec,plan,tasks}.md
src/
checkout.js # secret-free
config.js # a planted, clearly-fake AWS keysrc/config.js contains an intentionally-fake AWS-style key. It is the trigger for step 5 — the thing the guardrails gate must catch. Everything else is authored so analyze produces a real, non-trivial quality score.
Inspecting the artifacts
make sandbox leaves a fully-populated .forge/ behind so you can explore the exact state the gates produced:
scripts/sandbox/out/work/
.forge/
changes/001-checkout/
budget.yaml # the spend ledger
secscan.yaml # the waiver from step 6
session-log.jsonl # the attributed timeline
verify-attempts.yaml # the circuit-breaker state
site/index.html # the generated dashboardThe run prints the path to the generated dashboard at the end — open it in a browser to see the same spec-viewer a real project produces.
Sandbox, selftest, and e2e
Forge ships three overlapping checks. They are complementary, not redundant:
| Check | Command | Scope |
|---|---|---|
| Selftest | forge doctor --selftest | Fast in-process smoke test that the core commands are wired, using tiny synthetic stubs. |
| Sandbox | make sandbox | Drives the real binary through the full lifecycle against a realistic project, asserting each gate. Leaves artifacts to inspect. |
| e2e | make e2e | The sandbox scenario as a hermetic, CI-gated Go test — same assertions, no persisted state. |
Reach for the selftest to check the plumbing, the sandbox to see and inspect the experience, and make e2e in CI.
Extending it
The fixture and the driver are plain, versioned files:
- Add a spec or feature under
scripts/sandbox/fixtures/acme-shop/.specify/. - Add or adjust a step in
scripts/sandbox/run.sh(each step asserts an exit code and recordsPASS/FAIL). - Mirror any new assertion in
e2e/sandbox_test.goso CI covers it too.
Keep the two entry points in lockstep: whatever the human sandbox demonstrates, the hermetic e2e test should assert.
Next steps
- The enforcement doctrine — the enforced gates the sandbox exercises.
- The forge CLI — the real commands each step drives.
- Migration Importer — the import step the sandbox runs first.
- Spec-Viewer Dashboard — the dashboard the final step builds.
- Getting the Most out of Forge — apply the workflow to your own repo.