A small surface for agent-driven exploratory notebook work that
isn't yet ready to live in the tracked H → E → F artifact graph. A
sandbox is reversible (git checkout undoes everything), bounded
(one subdirectory per attempt), and explicitly outside kb_write_guard
validation — so the agent can iterate freely without tripping the
artifact discipline that exists for paper-load-bearing work.
| You are… | Use… |
|---|---|
| …iterating on a directional hunch; not sure it'll survive feasibility | sandbox |
| …confirming a calibrated prediction whose result will be cited | H### → E### → F### chain |
| …prototyping helpers / shapes / glue you might keep | sandbox |
| …running the production-scale grid | tracked experiment + queue |
The sandbox is autonomous-write territory for the agent: notebook
cells, kernel-written outputs, and helpers.py edits inside one
sandbox subdir don't require per-edit permission. Everything outside
the sandbox (package code, configs, kb/ artifacts, canon) remains
git-dance or explicit-permission. This boundary is encoded in the
scaffolded root README, not enforced by aexp tooling.
notebooks/
_sandbox/ ← sandbox root
README.md ← autonomy-boundary docs
.gitignore ← excludes *.npy, *.parquet, large/
2026-05-11_my-experiment/ ← per-experiment subdir
README.md ← experiment-design template
helpers.py ← sandbox-local utilities
00_feasibility.ipynb ← (user-created)
01_calibration.ipynb ← (user-created)
outputs/
plot.png ← tracked (small)
large/big_array.npy ← gitignored
The per-experiment subdir name is <YYYY-MM-DD>_<slug>. The
sandbox root (README.md + .gitignore) is created on the first
/aexp-new-sandbox invocation and preserved on subsequent runs — a
hand-edited root README is never overwritten.
The canonical entry point is /aexp-new-sandbox. From inside Claude
Code:
/aexp-new-sandbox
The skill asks for a slug (filesystem-safe; lowercase, hyphen-separated, alnum-only), optionally a human-readable title (defaults to a Title-Cased version of the slug), and optionally a parent directory override. It then calls the CLI verb described below and reports what it created.
aexp new-sandbox --slug "<slug>" \
[--title "<human title>"] \
[--parent-dir "<override>"]
Concrete example:
aexp new-sandbox --slug "prompt-brittleness" \
--title "Prompt-brittleness Characterization"Output (first invocation in a repo — initializes the sandbox root):
Created notebooks/_sandbox/2026-05-11_prompt-brittleness/
+ README.md
+ helpers.py
Initialized sandbox root:
+ notebooks/_sandbox/README.md
+ notebooks/_sandbox/.gitignore
Subsequent invocations skip the root-init step:
Created notebooks/_sandbox/2026-05-11_compositional-probe/
+ README.md
+ helpers.py
The CLI verb is idempotent at the directory-name level: re-running with
the same slug on the same date raises SandboxScaffoldError rather
than clobbering an existing experiment.
For programmatic use (e.g. test setup, a custom slash command):
from aexp.sandbox import scaffold
result = scaffold(
slug="prompt-brittleness",
title="Prompt-brittleness Characterization",
# parent_dir=..., optional override (default: notebooks/_sandbox)
# repo_root=..., optional explicit root (default: walk up from cwd)
# today=..., optional date_cls override (for deterministic tests)
)
print(result.dir_path) # "notebooks/_sandbox/2026-05-11_prompt-brittleness"
print(result.files_created) # ["...README.md", "...helpers.py", ...]
print(result.root_initialized) # True on first call in a reposcaffold() returns a frozen SandboxScaffoldResult dataclass with
slug, dir_name, dir_path (repo-relative POSIX), files_created,
and root_initialized. Raises SandboxScaffoldError on invalid slug
or pre-existing directory.
slugify(title) is also exported for callers that want to compute the
slug themselves; it matches aexp.artifacts.slugify so the same input
produces identical slugs across sandbox and tracked-artifact creation.
A sandbox notebook should start with:
from aexp.sandbox import setup_sandbox_notebook
ctx = setup_sandbox_notebook("2026-05-11_prompt-brittleness")
import helperssetup_sandbox_notebook(name) does three things:
- Walks up from
Path.cwd()to find the git repo root. - Locates
<repo>/notebooks/_sandbox/<name>/. - Inserts that directory at the front of
sys.pathsoimport helpersresolves to this experiment'shelpers.py, not some otherhelpersmodule shadowed by import-path order.
It returns {"repo_root": Path, "sandbox_dir": Path} so the notebook
can build absolute paths without re-doing the search.
Why this matters. On a remote Jupyter server the kernel's cwd
is the notebook's directory, not the repo root. Naive
Path("notebooks/...").resolve() from a notebook one sandbox dir deep
doubles the path (<root>/notebooks/_sandbox/<x>/notebooks/_sandbox/<x>)
and fails silently. setup_sandbox_notebook was the friction-design
fix for that gotcha (logged as a friction item in the consumer session
that motivated this surface).
A directional-experiment-design template with these sections:
- Mode (
exploratoryby default; sandbox is not paper-cite-able until promoted) - Origin context (pointer to the session note / motivation)
- Statement (directional, no numerical thresholds — refine via calibration)
- Mechanism (architectural / theoretical reason it might hold)
- Why this might generalize
- Shortcut risks (≥3 with mitigations)
- Test plan (val/test split, conditions, infrastructure used)
- Stages (Stage 0 — feasibility; Stage 1+ as needed)
- Open questions
- Cross-references
The template is biased toward exploratory framing — no fabricated confirm/reject thresholds, no presupposed numerical predictions. Fill the placeholders as the experiment takes shape; this is the right place to refine framing before promoting to a tracked H.
A small skeleton with SANDBOX_DIR and REPO_ROOT pre-resolved via
aexp.utils.paths.find_repo_root, plus a usage docstring. Edit freely
— this file is owned by this experiment subdir. If a helper proves
reusable across experiments, that's the cue to promote it to a real
package module (with the user in the loop, since that's a cross-cutting
change).
Describes the autonomy boundary, the <YYYY-MM-DD>_<slug>/ convention,
the promotion path through /aexp-new-thread → /aexp-new-hypothesis → /aexp-new-experiment → /aexp-promote-nb, and the small-vs-large
outputs distinction. Hand-editable; never re-overwritten by subsequent
/aexp-new-sandbox calls.
Excludes large output formats from version control while keeping the notebooks, helpers, and small artifacts tracked:
**/*.npy
**/*.parquet
**/*.h5
**/*.feather
**/outputs/large/**
**/.ipynb_checkpoints/
When a sandbox experiment's result is going to be cited as a paper finding, walk it into the tracked-artifact chain:
/aexp-new-thread— if no parent thread exists yet/aexp-new-hypothesis --thread T###— directional predictions; exploratory framing is fine for post-hoc hypotheses derived from sandbox smoke results/aexp-new-experiment --hypothesis H###— the formal experiment/aexp-promote-nb— extracts working cells from the sandbox notebook into a tracked-run Python script
Until you reach step 4, just iterate in the sandbox. There's no penalty for sandbox work that doesn't promote — that's the whole point of having a free-form workspace.
The sandbox surface was crystallized during a 2026-05-10 consumer session
that motivated the agentic-experiments AFK port. The original scaffold
lived in consumer-local Python before being lifted into aexp.sandbox.
The notebook first-cell convention (setup_sandbox_notebook) was designed
to close the friction that session documented — naive path resolution on
remote Jupyter doubling the sandbox path.
The directional-statement template — no fabricated thresholds, ≥3
shortcut risks, explicit "Mode: exploratory" framing — mirrors the
dual-mode ## Intent section added to the tracked-experiment template
in 0.2.0. Both exist to make THARKing-honest exploratory framing the
path of least resistance.